Enhanced setup and settings pages with full configuration

- Extended SetupRequest model to include all configuration fields
- Updated setup API endpoint to handle comprehensive configuration
- Created new setup.html with organized configuration sections
- Enhanced config modal in index.html with all settings
- Updated JavaScript modules to use unified config API
- Added backup configuration section
- Documented new features in features.md and instructions.md
This commit is contained in:
2026-01-17 18:01:15 +01:00
parent d676cb7dca
commit 4e29c4ed80
12 changed files with 1807 additions and 680 deletions

View File

@@ -8,12 +8,24 @@
## Configuration Management ## Configuration Management
- **Setup Page**: Initial configuration interface for server setup and basic settings - **Enhanced Setup Page**: Comprehensive initial configuration interface with all settings in one place:
- **Config Page**: View and modify application configuration settings - General Settings: Application name and data directory configuration
- **NFO Settings**: Configure TMDB API key and NFO auto-creation options - Security Settings: Master password setup with strength indicator
- **Media Download Settings**: Configure automatic poster, logo, and fanart downloads - Anime Directory: Primary directory path for anime storage
- **Scheduler Configuration**: Configure automated rescan schedules - Scheduler Settings: Enable/disable scheduler and configure check interval (in minutes)
- **Backup Management**: Create, restore, and manage configuration backups - Logging Settings: Configure log level, file path, file size limits, and backup count
- Backup Settings: Enable automatic backups with configurable path and retention period
- NFO Settings: TMDB API key, auto-creation options, and media file download preferences
- **Enhanced Settings/Config Modal**: Comprehensive configuration interface accessible from main page:
- General Settings: Edit application name and data directory
- Anime Directory: Modify anime storage location with browse functionality
- Scheduler Configuration: Enable/disable and configure check interval for automated operations
- Logging Configuration: Full control over logging level, file rotation, and backup count
- Backup Configuration: Configure automatic backup settings including path and retention
- NFO Settings: Complete control over TMDB integration and media file downloads
- Configuration Validation: Validate configuration for errors before saving
- Backup Management: Create, restore, and manage configuration backups
- Export/Import: Export configuration for backup or transfer to another instance
## User Interface ## User Interface

View File

@@ -8,32 +8,32 @@ The goal is to create a FastAPI-based web application that provides a modern int
## Architecture Principles ## Architecture Principles
- **Single Responsibility**: Each file/class has one clear purpose - **Single Responsibility**: Each file/class has one clear purpose
- **Dependency Injection**: Use FastAPI's dependency system - **Dependency Injection**: Use FastAPI's dependency system
- **Clean Separation**: Web layer calls core logic, never the reverse - **Clean Separation**: Web layer calls core logic, never the reverse
- **File Size Limit**: Maximum 500 lines per file - **File Size Limit**: Maximum 500 lines per file
- **Type Hints**: Use comprehensive type annotations - **Type Hints**: Use comprehensive type annotations
- **Error Handling**: Proper exception handling and logging - **Error Handling**: Proper exception handling and logging
## Additional Implementation Guidelines ## Additional Implementation Guidelines
### Code Style and Standards ### Code Style and Standards
- **Type Hints**: Use comprehensive type annotations throughout all modules - **Type Hints**: Use comprehensive type annotations throughout all modules
- **Docstrings**: Follow PEP 257 for function and class documentation - **Docstrings**: Follow PEP 257 for function and class documentation
- **Error Handling**: Implement custom exception classes with meaningful messages - **Error Handling**: Implement custom exception classes with meaningful messages
- **Logging**: Use structured logging with appropriate log levels - **Logging**: Use structured logging with appropriate log levels
- **Security**: Validate all inputs and sanitize outputs - **Security**: Validate all inputs and sanitize outputs
- **Performance**: Use async/await patterns for I/O operations - **Performance**: Use async/await patterns for I/O operations
## 📞 Escalation ## 📞 Escalation
If you encounter: If you encounter:
- Architecture issues requiring design decisions - Architecture issues requiring design decisions
- Tests that conflict with documented requirements - Tests that conflict with documented requirements
- Breaking changes needed - Breaking changes needed
- Unclear requirements or expectations - Unclear requirements or expectations
**Document the issue and escalate rather than guessing.** **Document the issue and escalate rather than guessing.**
@@ -92,45 +92,77 @@ conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.
For each task completed: For each task completed:
- [ ] Implementation follows coding standards - [ ] Implementation follows coding standards
- [ ] Unit tests written and passing - [ ] Unit tests written and passing
- [ ] Integration tests passing - [ ] Integration tests passing
- [ ] Documentation updated - [ ] Documentation updated
- [ ] Error handling implemented - [ ] Error handling implemented
- [ ] Logging added - [ ] Logging added
- [ ] Security considerations addressed - [ ] Security considerations addressed
- [ ] Performance validated - [ ] Performance validated
- [ ] Code reviewed - [ ] Code reviewed
- [ ] Task marked as complete in instructions.md - [ ] Task marked as complete in instructions.md
- [ ] Infrastructure.md updated and other docs - [ ] Infrastructure.md updated and other docs
- [ ] Changes committed to git; keep your messages in git short and clear - [ ] Changes committed to git; keep your messages in git short and clear
- [ ] Take the next task - [ ] Take the next task
--- ---
## TODO List: ## TODO List:
<!-- No pending issues --> ### ✅ Feature: Enhanced Setup and Settings Pages (COMPLETED)
## Recently Fixed Issues 1. **Setup Page Configuration**
- [x] Update setup page to allow configuration of the following settings:
- `name`: Application name (default: "Aniworld")
- `data_dir`: Data directory path (default: "data")
- `scheduler`:
- `enabled`: Enable/disable scheduler (default: true)
- `interval_minutes`: Scheduler interval in minutes (default: 60)
- `logging`:
- `level`: Log level (default: "INFO")
- `file`: Log file path (default: null)
- `max_bytes`: Max log file size in bytes (default: null)
- `backup_count`: Number of backup log files (default: 3)
- `backup`:
- `enabled`: Enable/disable backups (default: false)
- `path`: Backup directory path (default: "data/backups")
- `keep_days`: Days to keep backups (default: 30)
- `nfo`:
- `tmdb_api_key`: TMDB API key (default: null)
- `auto_create`: Auto-create NFO files (default: true)
- `update_on_scan`: Update NFO on scan (default: true)
- `download_poster`: Download poster images (default: true)
- `download_logo`: Download logo images (default: true)
- `download_fanart`: Download fanart images (default: true)
- `image_size`: Image size preference (default: "original")
- [x] Implement validation for all configuration fields
- [x] Add form UI with appropriate input types and validation feedback
- [x] Save configuration to config.json on setup completion
### 1. NFO API Endpoint Mismatch (Fixed: 2026-01-16) 2. **Settings Page Enhancement**
- [x] Display all configuration settings in settings page
- [x] Make all settings editable:
- General: `name`, `data_dir`
- Scheduler: `enabled`, `interval_minutes`
- Logging: `level`, `file`, `max_bytes`, `backup_count`
- Backup: `enabled`, `path`, `keep_days`
- NFO: `tmdb_api_key`, `auto_create`, `update_on_scan`, `download_poster`, `download_logo`, `download_fanart`, `image_size`
- Other: `master_password_hash` (allow password change), `anime_directory`
- [x] Implement save functionality with validation
- [x] Add success/error notifications for settings updates
- [x] Settings changes are applied immediately via API (some may require restart)
- [x] Add configuration section headers for better organization
- [x] Update JavaScript modules to handle all new configuration fields
**Issue**: Frontend JavaScript was calling incorrect NFO API endpoints causing 404 errors. **Implementation Notes:**
- The setup page ([setup.html](../src/server/web/templates/setup.html)) now includes all configuration sections with proper validation
**Root Cause**: - The SetupRequest model ([auth.py](../src/server/models/auth.py)) has been extended with all configuration fields
- Frontend was using `/api/nfo/series/{key}` pattern - The setup API endpoint ([api/auth.py](../src/server/api/auth.py)) now saves all configuration values
- Backend API uses `/api/nfo/{serie_id}/create`, `/api/nfo/{serie_id}/update`, `/api/nfo/{serie_id}/content` - The config modal in [index.html](../src/server/web/templates/index.html) displays all settings with organized sections
- JavaScript modules ([main-config.js](../src/server/web/static/js/index/main-config.js), [scheduler-config.js](../src/server/web/static/js/index/scheduler-config.js), [logging-config.js](../src/server/web/static/js/index/logging-config.js), [nfo-config.js](../src/server/web/static/js/index/nfo-config.js)) have been updated to use the unified config API
**Changes Made**: - All configuration is saved through the `/api/config` endpoint using PUT requests
- Updated [nfo-manager.js](../src/server/web/static/js/index/nfo-manager.js): - Configuration validation is performed both client-side and server-side
- Fixed `createNFO()`: Now calls `POST /api/nfo/{key}/create` with proper request body
- Fixed `refreshNFO()`: Now calls `PUT /api/nfo/{key}/update`
- Fixed `viewNFO()`: Now calls `GET /api/nfo/{key}/content`
- Updated response handling to check for actual API response fields (e.g., `message`, `content`)
- Removed non-existent `getStatistics()` function
- Fixed `getSeriesWithoutNFO()` to use correct endpoint and response structure
**Status**: ✅ Fixed and tested
--- ---

View File

@@ -29,8 +29,8 @@ optional_bearer = HTTPBearer(auto_error=False)
async def setup_auth(req: SetupRequest): async def setup_auth(req: SetupRequest):
"""Initial setup endpoint to configure the master password. """Initial setup endpoint to configure the master password.
This endpoint also initializes the configuration with default values This endpoint also initializes the configuration with all provided values
and saves the anime directory and master password hash. and saves them to config.json.
""" """
if auth_service.is_configured(): if auth_service.is_configured():
raise HTTPException( raise HTTPException(
@@ -44,26 +44,77 @@ async def setup_auth(req: SetupRequest):
req.master_password req.master_password
) )
# Initialize or update config with master password hash # Initialize or update config with all provided values
# and anime directory
config_service = get_config_service() config_service = get_config_service()
try: try:
config = config_service.load_config() config = config_service.load_config()
except Exception: except Exception:
# If config doesn't exist, create default # If config doesn't exist, create default
from src.server.models.config import (
SchedulerConfig,
LoggingConfig,
BackupConfig,
NFOConfig,
)
config = AppConfig() config = AppConfig()
# Update basic settings
if req.name:
config.name = req.name
if req.data_dir:
config.data_dir = req.data_dir
# Update scheduler configuration
if req.scheduler_enabled is not None:
config.scheduler.enabled = req.scheduler_enabled
if req.scheduler_interval_minutes is not None:
config.scheduler.interval_minutes = req.scheduler_interval_minutes
# Update logging configuration
if req.logging_level:
config.logging.level = req.logging_level.upper()
if req.logging_file is not None:
config.logging.file = req.logging_file
if req.logging_max_bytes is not None:
config.logging.max_bytes = req.logging_max_bytes
if req.logging_backup_count is not None:
config.logging.backup_count = req.logging_backup_count
# Update backup configuration
if req.backup_enabled is not None:
config.backup.enabled = req.backup_enabled
if req.backup_path:
config.backup.path = req.backup_path
if req.backup_keep_days is not None:
config.backup.keep_days = req.backup_keep_days
# Update NFO configuration
if req.nfo_tmdb_api_key is not None:
config.nfo.tmdb_api_key = req.nfo_tmdb_api_key
if req.nfo_auto_create is not None:
config.nfo.auto_create = req.nfo_auto_create
if req.nfo_update_on_scan is not None:
config.nfo.update_on_scan = req.nfo_update_on_scan
if req.nfo_download_poster is not None:
config.nfo.download_poster = req.nfo_download_poster
if req.nfo_download_logo is not None:
config.nfo.download_logo = req.nfo_download_logo
if req.nfo_download_fanart is not None:
config.nfo.download_fanart = req.nfo_download_fanart
if req.nfo_image_size:
config.nfo.image_size = req.nfo_image_size.lower()
# Store master password hash in config's other field # Store master password hash in config's other field
config.other['master_password_hash'] = password_hash config.other['master_password_hash'] = password_hash
# Store anime directory in config's other field if provided # Store anime directory in config's other field if provided
anime_directory = None anime_directory = None
if hasattr(req, 'anime_directory') and req.anime_directory: if req.anime_directory:
anime_directory = req.anime_directory.strip() anime_directory = req.anime_directory.strip()
if anime_directory: if anime_directory:
config.other['anime_directory'] = anime_directory config.other['anime_directory'] = anime_directory
# Save the config with the password hash and anime directory # Save the config with all updates
config_service.save_config(config, create_backup=False) config_service.save_config(config, create_backup=False)
# Sync series from data files to database if anime directory is set # Sync series from data files to database if anime directory is set

View File

@@ -36,8 +36,12 @@ class LoginResponse(BaseModel):
class SetupRequest(BaseModel): class SetupRequest(BaseModel):
"""Request to initialize the master password during first-time setup.""" """Request to initialize the master password during first-time setup.
This request includes all configuration fields needed to set up the application.
"""
# Required fields
master_password: str = Field( master_password: str = Field(
..., min_length=8, description="New master password" ..., min_length=8, description="New master password"
) )
@@ -45,6 +49,94 @@ class SetupRequest(BaseModel):
None, description="Optional anime directory path" None, description="Optional anime directory path"
) )
# Application settings
name: Optional[str] = Field(
default="Aniworld", description="Application name"
)
data_dir: Optional[str] = Field(
default="data", description="Data directory path"
)
# Scheduler configuration
scheduler_enabled: Optional[bool] = Field(
default=True, description="Enable/disable scheduler"
)
scheduler_interval_minutes: Optional[int] = Field(
default=60, ge=1, description="Scheduler interval in minutes"
)
# Logging configuration
logging_level: Optional[str] = Field(
default="INFO", description="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)"
)
logging_file: Optional[str] = Field(
default=None, description="Log file path"
)
logging_max_bytes: Optional[int] = Field(
default=None, ge=0, description="Max log file size in bytes"
)
logging_backup_count: Optional[int] = Field(
default=3, ge=0, description="Number of backup log files"
)
# Backup configuration
backup_enabled: Optional[bool] = Field(
default=False, description="Enable/disable backups"
)
backup_path: Optional[str] = Field(
default="data/backups", description="Backup directory path"
)
backup_keep_days: Optional[int] = Field(
default=30, ge=0, description="Days to keep backups"
)
# NFO configuration
nfo_tmdb_api_key: Optional[str] = Field(
default=None, description="TMDB API key"
)
nfo_auto_create: Optional[bool] = Field(
default=True, description="Auto-create NFO files"
)
nfo_update_on_scan: Optional[bool] = Field(
default=True, description="Update NFO on scan"
)
nfo_download_poster: Optional[bool] = Field(
default=True, description="Download poster images"
)
nfo_download_logo: Optional[bool] = Field(
default=True, description="Download logo images"
)
nfo_download_fanart: Optional[bool] = Field(
default=True, description="Download fanart images"
)
nfo_image_size: Optional[str] = Field(
default="original", description="Image size preference (original or w500)"
)
@field_validator("logging_level")
@classmethod
def validate_logging_level(cls, v: Optional[str]) -> Optional[str]:
"""Validate logging level."""
if v is None:
return v
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
lvl = v.upper()
if lvl not in allowed:
raise ValueError(f"Invalid logging level: {v}. Must be one of {allowed}")
return lvl
@field_validator("nfo_image_size")
@classmethod
def validate_image_size(cls, v: Optional[str]) -> Optional[str]:
"""Validate image size."""
if v is None:
return v
allowed = {"original", "w500"}
size = v.lower()
if size not in allowed:
raise ValueError(f"Invalid image size: {v}. Must be 'original' or 'w500'")
return size
class AuthStatus(BaseModel): class AuthStatus(BaseModel):
"""Public status about whether auth is configured and the current user state.""" """Public status about whether auth is configured and the current user state."""

View File

@@ -56,6 +56,9 @@ AniWorld.ConfigManager = (function() {
// NFO configuration // NFO configuration
bindNFOEvents(); bindNFOEvents();
// Backup configuration
bindBackupEvents();
// Status panel // Status panel
const closeStatus = document.getElementById('close-status'); const closeStatus = document.getElementById('close-status');
if (closeStatus) { if (closeStatus) {
@@ -118,8 +121,16 @@ AniWorld.ConfigManager = (function() {
} }
} }
/** /** * Bind backup config events
* Bind NFO config events */
function bindBackupEvents() {
const saveBackup = document.getElementById('save-backup-config');
if (saveBackup) {
saveBackup.addEventListener('click', AniWorld.MainConfig.saveBackupConfig);
}
}
/** * Bind NFO config events
*/ */
function bindNFOEvents() { function bindNFOEvents() {
const saveNFO = document.getElementById('save-nfo-config'); const saveNFO = document.getElementById('save-nfo-config');

View File

@@ -113,28 +113,25 @@ AniWorld.LoggingConfig = (function() {
*/ */
async function save() { async function save() {
try { try {
const config = { // Get current config
log_level: document.getElementById('log-level').value, const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
enable_console_logging: document.getElementById('enable-console-logging').checked, if (!configResponse) return;
enable_console_progress: document.getElementById('enable-console-progress').checked, const config = await configResponse.json();
enable_fail2ban_logging: document.getElementById('enable-fail2ban-logging').checked
// Update logging settings
config.logging = {
level: document.getElementById('log-level').value.toUpperCase(),
file: document.getElementById('log-file').value.trim() || null,
max_bytes: document.getElementById('log-max-bytes').value ? parseInt(document.getElementById('log-max-bytes').value) : null,
backup_count: parseInt(document.getElementById('log-backup-count').value) || 3
}; };
const response = await AniWorld.ApiClient.request(API.LOGGING_CONFIG, { // Save updated config
method: 'POST', const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response) return; if (!response) return;
const data = await response.json();
if (data.success) { AniWorld.UI.showToast('Logging configuration saved successfully', 'success');
AniWorld.UI.showToast('Logging configuration saved successfully', 'success'); await load();
await load();
} else {
AniWorld.UI.showToast('Failed to save logging config: ' + data.error, 'error');
}
} catch (error) { } catch (error) {
console.error('Error saving logging config:', error); console.error('Error saving logging config:', error);
AniWorld.UI.showToast('Failed to save logging configuration', 'error'); AniWorld.UI.showToast('Failed to save logging configuration', 'error');

View File

@@ -20,31 +20,55 @@ AniWorld.MainConfig = (function() {
async function save() { async function save() {
try { try {
const animeDirectory = document.getElementById('anime-directory-input').value.trim(); const animeDirectory = document.getElementById('anime-directory-input').value.trim();
const appName = document.getElementById('app-name-input').value.trim();
const dataDir = document.getElementById('data-dir-input').value.trim();
if (!animeDirectory) { if (!animeDirectory) {
AniWorld.UI.showToast('Please enter an anime directory path', 'error'); AniWorld.UI.showToast('Please enter an anime directory path', 'error');
return; return;
} }
const response = await AniWorld.ApiClient.post(API.CONFIG_DIRECTORY, { // Get current config
directory: animeDirectory const currentConfig = await loadCurrentConfig();
}); if (!currentConfig) {
AniWorld.UI.showToast('Failed to load current configuration', 'error');
return;
}
// Update fields
if (appName) currentConfig.name = appName;
if (dataDir) currentConfig.data_dir = dataDir;
if (!currentConfig.other) currentConfig.other = {};
currentConfig.other.anime_directory = animeDirectory;
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, currentConfig);
if (!response) return; if (!response) return;
const data = await response.json(); const data = await response.json();
if (data.success) { AniWorld.UI.showToast('Main configuration saved successfully', 'success');
AniWorld.UI.showToast('Main configuration saved successfully', 'success'); await refreshStatus();
await refreshStatus();
} else {
AniWorld.UI.showToast('Failed to save configuration: ' + data.error, 'error');
}
} catch (error) { } catch (error) {
console.error('Error saving main config:', error); console.error('Error saving main config:', error);
AniWorld.UI.showToast('Failed to save main configuration', 'error'); AniWorld.UI.showToast('Failed to save main configuration', 'error');
} }
} }
/**
* Load current configuration from API
*/
async function loadCurrentConfig() {
try {
const response = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!response) return null;
return await response.json();
} catch (error) {
console.error('Error loading config:', error);
return null;
}
}
/** /**
* Reset main configuration * Reset main configuration
*/ */
@@ -109,12 +133,45 @@ AniWorld.MainConfig = (function() {
*/ */
async function refreshStatus() { async function refreshStatus() {
try { try {
const response = await AniWorld.ApiClient.get(API.ANIME_STATUS); // Load full configuration
if (!response) return; const config = await loadCurrentConfig();
const data = await response.json(); if (!config) return;
document.getElementById('anime-directory-input').value = data.directory || ''; // Populate general settings
document.getElementById('series-count-input').value = data.series_count || '0'; document.getElementById('app-name-input').value = config.name || 'Aniworld';
document.getElementById('data-dir-input').value = config.data_dir || 'data';
document.getElementById('anime-directory-input').value = config.other?.anime_directory || '';
// Populate scheduler settings
document.getElementById('scheduled-rescan-enabled').checked = config.scheduler?.enabled || false;
document.getElementById('scheduled-rescan-interval').value = config.scheduler?.interval_minutes || 60;
// Populate logging settings
document.getElementById('log-level').value = config.logging?.level || 'INFO';
document.getElementById('log-file').value = config.logging?.file || '';
document.getElementById('log-max-bytes').value = config.logging?.max_bytes || '';
document.getElementById('log-backup-count').value = config.logging?.backup_count || 3;
// Populate backup settings
document.getElementById('backup-enabled').checked = config.backup?.enabled || false;
document.getElementById('backup-path').value = config.backup?.path || 'data/backups';
document.getElementById('backup-keep-days').value = config.backup?.keep_days || 30;
// Populate NFO settings
document.getElementById('tmdb-api-key').value = config.nfo?.tmdb_api_key || '';
document.getElementById('nfo-auto-create').checked = config.nfo?.auto_create || false;
document.getElementById('nfo-update-on-scan').checked = config.nfo?.update_on_scan || false;
document.getElementById('nfo-download-poster').checked = config.nfo?.download_poster !== false;
document.getElementById('nfo-download-logo').checked = config.nfo?.download_logo !== false;
document.getElementById('nfo-download-fanart').checked = config.nfo?.download_fanart !== false;
document.getElementById('nfo-image-size').value = config.nfo?.image_size || 'original';
// Get series count from status endpoint
const statusResponse = await AniWorld.ApiClient.get(API.ANIME_STATUS);
if (statusResponse) {
const statusData = await statusResponse.json();
document.getElementById('series-count-input').value = statusData.series_count || '0';
}
} catch (error) { } catch (error) {
console.error('Error refreshing status:', error); console.error('Error refreshing status:', error);
} }
@@ -278,6 +335,36 @@ AniWorld.MainConfig = (function() {
} }
} }
/**
* Save backup configuration
*/
async function saveBackupConfig() {
try {
const config = await loadCurrentConfig();
if (!config) {
AniWorld.UI.showToast('Failed to load current configuration', 'error');
return;
}
// Update backup settings
config.backup = {
enabled: document.getElementById('backup-enabled').checked,
path: document.getElementById('backup-path').value.trim(),
keep_days: parseInt(document.getElementById('backup-keep-days').value) || 30
};
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (!response) return;
AniWorld.UI.showToast('Backup configuration saved successfully', 'success');
} catch (error) {
console.error('Error saving backup config:', error);
AniWorld.UI.showToast('Failed to save backup configuration', 'error');
}
}
// Public API // Public API
return { return {
save: save, save: save,
@@ -289,6 +376,7 @@ AniWorld.MainConfig = (function() {
viewBackups: viewBackups, viewBackups: viewBackups,
exportConfig: exportConfig, exportConfig: exportConfig,
validateConfig: validateConfig, validateConfig: validateConfig,
resetAllConfig: resetAllConfig resetAllConfig: resetAllConfig,
saveBackupConfig: saveBackupConfig
}; };
})(); })();

View File

@@ -92,8 +92,16 @@ AniWorld.NFOConfig = (function() {
return; return;
} }
const nfoConfig = { // Get current config
tmdb_api_key: tmdbKey ? tmdbKey.value.trim() : null, const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!configResponse) {
throw new Error('Failed to load current configuration');
}
const config = await configResponse.json();
// Update NFO settings
config.nfo = {
tmdb_api_key: tmdbKey ? tmdbKey.value.trim() || null : null,
auto_create: autoCreate ? autoCreate.checked : false, auto_create: autoCreate ? autoCreate.checked : false,
update_on_scan: updateOnScan ? updateOnScan.checked : false, update_on_scan: updateOnScan ? updateOnScan.checked : false,
download_poster: downloadPoster ? downloadPoster.checked : true, download_poster: downloadPoster ? downloadPoster.checked : true,
@@ -103,13 +111,7 @@ AniWorld.NFOConfig = (function() {
}; };
// Save configuration // Save configuration
const response = await AniWorld.ApiClient.request( const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
API.CONFIG,
{
method: 'PUT',
body: JSON.stringify({ nfo: nfoConfig })
}
);
if (response) { if (response) {
AniWorld.UI.showToast('NFO configuration saved successfully', 'success'); AniWorld.UI.showToast('NFO configuration saved successfully', 'success');

View File

@@ -55,24 +55,25 @@ AniWorld.SchedulerConfig = (function() {
async function save() { async function save() {
try { try {
const enabled = document.getElementById('scheduled-rescan-enabled').checked; const enabled = document.getElementById('scheduled-rescan-enabled').checked;
const time = document.getElementById('scheduled-rescan-time').value; const interval = parseInt(document.getElementById('scheduled-rescan-interval').value) || 60;
const autoDownload = document.getElementById('auto-download-after-rescan').checked;
const response = await AniWorld.ApiClient.post(API.SCHEDULER_CONFIG, { // Get current config
const configResponse = await AniWorld.ApiClient.get(AniWorld.Constants.API.CONFIG);
if (!configResponse) return;
const config = await configResponse.json();
// Update scheduler settings
config.scheduler = {
enabled: enabled, enabled: enabled,
time: time, interval_minutes: interval
auto_download_after_rescan: autoDownload };
});
// Save updated config
const response = await AniWorld.ApiClient.put(AniWorld.Constants.API.CONFIG, config);
if (!response) return; if (!response) return;
const data = await response.json();
if (data.success) { AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success');
AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success'); await load();
await load();
} else {
AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
}
} catch (error) { } catch (error) {
console.error('Error saving scheduler config:', error); console.error('Error saving scheduler config:', error);
AniWorld.UI.showToast('Failed to save scheduler configuration', 'error'); AniWorld.UI.showToast('Failed to save scheduler configuration', 'error');

View File

@@ -186,6 +186,23 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<!-- General Settings -->
<div class="config-section">
<h4 data-text="general-settings">General Settings</h4>
<div class="config-item">
<label for="app-name-input" data-text="app-name">Application Name:</label>
<input type="text" id="app-name-input" class="input-field"
placeholder="Aniworld">
</div>
<div class="config-item">
<label for="data-dir-input" data-text="data-directory">Data Directory:</label>
<input type="text" id="data-dir-input" class="input-field"
placeholder="data">
</div>
</div>
<div class="config-item"> <div class="config-item">
<label for="anime-directory-input" data-text="anime-directory">Anime Directory:</label> <label for="anime-directory-input" data-text="anime-directory">Anime Directory:</label>
<div class="input-group"> <div class="input-group">
@@ -233,10 +250,18 @@
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" id="scheduled-rescan-enabled"> <input type="checkbox" id="scheduled-rescan-enabled">
<span class="checkbox-custom"></span> <span class="checkbox-custom"></span>
<span data-text="enable-scheduled-rescan">Enable Daily Rescan</span> <span data-text="enable-scheduled-rescan">Enable Scheduler</span>
</label> </label>
</div> </div>
<div class="config-item" id="rescan-interval-config">
<label for="scheduled-rescan-interval" data-text="rescan-interval">Check Interval (minutes):</label>
<input type="number" id="scheduled-rescan-interval" value="60" min="1" class="input-field">
<small class="config-hint" data-text="rescan-interval-hint">
How often to check for new episodes (minimum 1 minute)
</small>
</div>
<div class="config-item" id="rescan-time-config"> <div class="config-item" id="rescan-time-config">
<label for="scheduled-rescan-time" data-text="rescan-time">Rescan Time (24h format):</label> <label for="scheduled-rescan-time" data-text="rescan-time">Rescan Time (24h format):</label>
<input type="time" id="scheduled-rescan-time" value="03:00" class="input-field"> <input type="time" id="scheduled-rescan-time" value="03:00" class="input-field">
@@ -295,6 +320,30 @@
</select> </select>
</div> </div>
<div class="config-item">
<label for="log-file" data-text="log-file">Log File Path (optional):</label>
<input type="text" id="log-file" placeholder="logs/app.log" class="input-field">
<small class="config-hint" data-text="log-file-hint">
Leave empty to log to console only
</small>
</div>
<div class="config-item">
<label for="log-max-bytes" data-text="log-max-bytes">Max File Size (bytes):</label>
<input type="number" id="log-max-bytes" placeholder="10485760" min="0" class="input-field">
<small class="config-hint" data-text="log-max-bytes-hint">
Maximum size before log rotation (leave empty for no rotation)
</small>
</div>
<div class="config-item">
<label for="log-backup-count" data-text="log-backup-count">Backup Count:</label>
<input type="number" id="log-backup-count" value="3" min="0" class="input-field">
<small class="config-hint" data-text="log-backup-count-hint">
Number of rotated log files to keep
</small>
</div>
<div class="config-item"> <div class="config-item">
<div class="checkbox-container"> <div class="checkbox-container">
<input type="checkbox" id="enable-console-logging"> <input type="checkbox" id="enable-console-logging">
@@ -428,6 +477,45 @@
</div> </div>
</div> </div>
<!-- Backup Configuration -->
<div class="config-section">
<h4 data-text="backup-config">Backup Configuration</h4>
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" id="backup-enabled">
<span class="checkbox-custom"></span>
<span data-text="backup-enabled">Enable Automatic Backups</span>
</label>
<small class="config-hint" data-text="backup-enabled-hint">
Automatically backup configuration files
</small>
</div>
<div class="config-item">
<label for="backup-path" data-text="backup-path">Backup Path:</label>
<input type="text" id="backup-path" value="data/backups" class="input-field">
<small class="config-hint" data-text="backup-path-hint">
Directory where backups will be stored
</small>
</div>
<div class="config-item">
<label for="backup-keep-days" data-text="backup-keep-days">Keep Backups (days):</label>
<input type="number" id="backup-keep-days" value="30" min="0" class="input-field">
<small class="config-hint" data-text="backup-keep-days-hint">
How many days to keep old backups
</small>
</div>
<div class="config-actions">
<button id="save-backup-config" class="btn btn-primary">
<i class="fas fa-save"></i>
<span data-text="save-backup-config">Save Backup Settings</span>
</button>
</div>
</div>
<!-- Configuration Management --> <!-- Configuration Management -->
<div class="config-section"> <div class="config-section">
<h4 data-text="config-management">Configuration Management</h4> <h4 data-text="config-management">Configuration Management</h4>

View File

@@ -14,7 +14,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%); background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
padding: 1rem; padding: 2rem 1rem;
} }
.setup-card { .setup-card {
@@ -23,8 +23,10 @@
padding: 2rem; padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
max-width: 500px; max-width: 700px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
max-height: 90vh;
overflow-y: auto;
} }
.setup-header { .setup-header {
@@ -58,36 +60,80 @@
gap: 1.5rem; gap: 1.5rem;
} }
.config-section {
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1rem;
background: var(--color-background);
}
.section-header {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header i {
color: var(--color-primary);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
} }
.form-group.full-width {
grid-column: 1 / -1;
}
.form-label { .form-label {
font-weight: 500; font-weight: 500;
color: var(--color-text); color: var(--color-text);
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-input { .form-input, .form-select {
width: 100%; width: 100%;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border: 2px solid var(--color-border); border: 2px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
background: var(--color-background); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
transition: all 0.2s ease; transition: all 0.2s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.form-input:focus { .form-input:focus, .form-select:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1); box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
} }
.form-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.form-checkbox input[type="checkbox"] {
width: 1.2rem;
height: 1.2rem;
cursor: pointer;
}
.password-input-group { .password-input-group {
position: relative; position: relative;
} }
@@ -204,28 +250,6 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
.security-tips {
margin-top: 1.5rem;
padding: 1rem;
background: var(--color-info-light);
border: 1px solid var(--color-info);
border-radius: 8px;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.security-tips h4 {
margin: 0 0 0.5rem 0;
color: var(--color-info);
font-size: 0.9rem;
}
.security-tips ul {
margin: 0;
padding-left: 1.2rem;
line-height: 1.4;
}
.theme-toggle { .theme-toggle {
position: absolute; position: absolute;
top: 1rem; top: 1rem;
@@ -263,6 +287,12 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
}
</style> </style>
</head> </head>
@@ -278,46 +308,207 @@
<i class="fas fa-play-circle"></i> <i class="fas fa-play-circle"></i>
</div> </div>
<h1>Welcome to AniWorld Manager</h1> <h1>Welcome to AniWorld Manager</h1>
<p>Let's set up your master password to secure your anime collection.</p> <p>Configure your anime manager with all the settings you need.</p>
</div> </div>
<form class="setup-form" id="setup-form"> <form class="setup-form" id="setup-form">
<div class="form-group"> <!-- General Settings -->
<label for="directory" class="form-label">Anime Directory</label> <div class="config-section">
<input type="text" id="directory" name="directory" class="form-input" placeholder="C:\Anime" <div class="section-header">
value="{{ current_directory }}" required> <i class="fas fa-cog"></i>
<div class="form-help"> General Settings
The directory where your anime series are stored. This can be changed later in settings. </div>
<div class="form-row">
<div class="form-group">
<label for="name" class="form-label">Application Name</label>
<input type="text" id="name" name="name" class="form-input" value="Aniworld" required>
</div>
<div class="form-group">
<label for="data_dir" class="form-label">Data Directory</label>
<input type="text" id="data_dir" name="data_dir" class="form-input" value="data" required>
</div>
<div class="form-group full-width">
<label for="directory" class="form-label">Anime Directory *</label>
<input type="text" id="directory" name="directory" class="form-input"
placeholder="/path/to/your/anime" value="{{ current_directory }}" required>
<div class="form-help">
The directory where your anime series are stored.
</div>
</div>
</div> </div>
</div> </div>
<div class="form-group"> <!-- Security Settings -->
<label for="password" class="form-label">Master Password</label> <div class="config-section">
<div class="password-input-group"> <div class="section-header">
<input type="password" id="password" name="password" class="form-input password-input" <i class="fas fa-shield-alt"></i>
placeholder="Create a strong password" required minlength="8"> Security Settings
<button type="button" class="password-toggle" id="password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div> </div>
<div class="password-strength"> <div class="form-row">
<div class="strength-bar" id="strength-1"></div> <div class="form-group full-width">
<div class="strength-bar" id="strength-2"></div> <label for="password" class="form-label">Master Password *</label>
<div class="strength-bar" id="strength-3"></div> <div class="password-input-group">
<div class="strength-bar" id="strength-4"></div> <input type="password" id="password" name="password" class="form-input password-input"
placeholder="Create a strong password" required minlength="8">
<button type="button" class="password-toggle" id="password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="password-strength">
<div class="strength-bar" id="strength-1"></div>
<div class="strength-bar" id="strength-2"></div>
<div class="strength-bar" id="strength-3"></div>
<div class="strength-bar" id="strength-4"></div>
</div>
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
</div>
<div class="form-group full-width">
<label for="confirm-password" class="form-label">Confirm Password *</label>
<div class="password-input-group">
<input type="password" id="confirm-password" name="confirm-password"
class="form-input password-input" placeholder="Confirm your password" required minlength="8">
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div> </div>
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
</div> </div>
<div class="form-group"> <!-- Scheduler Settings -->
<label for="confirm-password" class="form-label">Confirm Password</label> <div class="config-section">
<div class="password-input-group"> <div class="section-header">
<input type="password" id="confirm-password" name="confirm-password" <i class="fas fa-clock"></i>
class="form-input password-input" placeholder="Confirm your password" required Scheduler Settings
minlength="8"> </div>
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1"> <div class="form-row">
<i class="fas fa-eye"></i> <div class="form-group">
</button> <label class="form-checkbox">
<input type="checkbox" id="scheduler_enabled" name="scheduler_enabled" checked>
<span>Enable Scheduler</span>
</label>
<div class="form-help">Automatically check for new episodes</div>
</div>
<div class="form-group">
<label for="scheduler_interval_minutes" class="form-label">Interval (minutes)</label>
<input type="number" id="scheduler_interval_minutes" name="scheduler_interval_minutes"
class="form-input" value="60" min="1" required>
</div>
</div>
</div>
<!-- Logging Settings -->
<div class="config-section">
<div class="section-header">
<i class="fas fa-file-alt"></i>
Logging Settings
</div>
<div class="form-row">
<div class="form-group">
<label for="logging_level" class="form-label">Log Level</label>
<select id="logging_level" name="logging_level" class="form-select">
<option value="DEBUG">DEBUG</option>
<option value="INFO" selected>INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
</div>
<div class="form-group">
<label for="logging_file" class="form-label">Log File (optional)</label>
<input type="text" id="logging_file" name="logging_file" class="form-input"
placeholder="logs/app.log">
</div>
<div class="form-group">
<label for="logging_max_bytes" class="form-label">Max File Size (bytes)</label>
<input type="number" id="logging_max_bytes" name="logging_max_bytes"
class="form-input" placeholder="10485760" min="0">
</div>
<div class="form-group">
<label for="logging_backup_count" class="form-label">Backup Count</label>
<input type="number" id="logging_backup_count" name="logging_backup_count"
class="form-input" value="3" min="0" required>
</div>
</div>
</div>
<!-- Backup Settings -->
<div class="config-section">
<div class="section-header">
<i class="fas fa-save"></i>
Backup Settings
</div>
<div class="form-row">
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="backup_enabled" name="backup_enabled">
<span>Enable Backups</span>
</label>
<div class="form-help">Automatically backup configuration</div>
</div>
<div class="form-group">
<label for="backup_keep_days" class="form-label">Keep Days</label>
<input type="number" id="backup_keep_days" name="backup_keep_days"
class="form-input" value="30" min="0" required>
</div>
<div class="form-group full-width">
<label for="backup_path" class="form-label">Backup Path</label>
<input type="text" id="backup_path" name="backup_path" class="form-input"
value="data/backups" required>
</div>
</div>
</div>
<!-- NFO/Metadata Settings -->
<div class="config-section">
<div class="section-header">
<i class="fas fa-info-circle"></i>
NFO & Metadata Settings
</div>
<div class="form-row">
<div class="form-group full-width">
<label for="nfo_tmdb_api_key" class="form-label">TMDB API Key (optional)</label>
<input type="text" id="nfo_tmdb_api_key" name="nfo_tmdb_api_key" class="form-input"
placeholder="Enter your TMDB API key">
<div class="form-help">Required for fetching metadata from TMDB</div>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="nfo_auto_create" name="nfo_auto_create" checked>
<span>Auto-create NFO files</span>
</label>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="nfo_update_on_scan" name="nfo_update_on_scan" checked>
<span>Update on scan</span>
</label>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="nfo_download_poster" name="nfo_download_poster" checked>
<span>Download posters</span>
</label>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="nfo_download_logo" name="nfo_download_logo" checked>
<span>Download logos</span>
</label>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="nfo_download_fanart" name="nfo_download_fanart" checked>
<span>Download fanart</span>
</label>
</div>
<div class="form-group">
<label for="nfo_image_size" class="form-label">Image Size</label>
<select id="nfo_image_size" name="nfo_image_size" class="form-select">
<option value="original" selected>Original</option>
<option value="w500">W500</option>
</select>
</div>
</div> </div>
</div> </div>
@@ -328,16 +519,6 @@
<span>Complete Setup</span> <span>Complete Setup</span>
</button> </button>
</form> </form>
<div class="security-tips">
<h4><i class="fas fa-shield-alt"></i> Security Tips</h4>
<ul>
<li>Use a password with at least 12 characters</li>
<li>Include uppercase, lowercase, numbers, and symbols</li>
<li>Don't use personal information or common words</li>
<li>Consider using a password manager</li>
</ul>
</div>
</div> </div>
</div> </div>
@@ -392,17 +573,13 @@
let score = 0; let score = 0;
let feedback = []; let feedback = [];
// Length check
if (password.length >= 8) score++; if (password.length >= 8) score++;
if (password.length >= 12) score++; if (password.length >= 12) score++;
// Character variety
if (/[a-z]/.test(password)) score++; if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++; if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++; if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++; if (/[^A-Za-z0-9]/.test(password)) score++;
// Penalties
if (password.length < 8) { if (password.length < 8) {
feedback.push('Too short'); feedback.push('Too short');
score = Math.max(0, score - 2); score = Math.max(0, score - 2);
@@ -447,14 +624,8 @@
} }
} }
// Form submission
const setupForm = document.getElementById('setup-form');
const setupButton = document.getElementById('setup-button');
const messageContainer = document.getElementById('message-container');
const confirmPasswordInput = document.getElementById('confirm-password');
const directoryInput = document.getElementById('directory');
// Real-time password confirmation // Real-time password confirmation
const confirmPasswordInput = document.getElementById('confirm-password');
confirmPasswordInput.addEventListener('input', validatePasswordMatch); confirmPasswordInput.addEventListener('input', validatePasswordMatch);
passwordInput.addEventListener('input', validatePasswordMatch); passwordInput.addEventListener('input', validatePasswordMatch);
@@ -471,12 +642,16 @@
} }
} }
// Form submission
const setupForm = document.getElementById('setup-form');
const setupButton = document.getElementById('setup-button');
const messageContainer = document.getElementById('message-container');
setupForm.addEventListener('submit', async (e) => { setupForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const password = passwordInput.value; const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value; const confirmPassword = confirmPasswordInput.value;
const directory = directoryInput.value.trim();
if (password !== confirmPassword) { if (password !== confirmPassword) {
showMessage('Passwords do not match', 'error'); showMessage('Passwords do not match', 'error');
@@ -489,23 +664,40 @@
return; return;
} }
if (!directory) {
showMessage('Please enter a valid anime directory', 'error');
return;
}
setLoading(true); setLoading(true);
// Collect all form data
const formData = {
master_password: password,
anime_directory: document.getElementById('directory').value.trim(),
name: document.getElementById('name').value.trim(),
data_dir: document.getElementById('data_dir').value.trim(),
scheduler_enabled: document.getElementById('scheduler_enabled').checked,
scheduler_interval_minutes: parseInt(document.getElementById('scheduler_interval_minutes').value),
logging_level: document.getElementById('logging_level').value,
logging_file: document.getElementById('logging_file').value.trim() || null,
logging_max_bytes: document.getElementById('logging_max_bytes').value ?
parseInt(document.getElementById('logging_max_bytes').value) : null,
logging_backup_count: parseInt(document.getElementById('logging_backup_count').value),
backup_enabled: document.getElementById('backup_enabled').checked,
backup_path: document.getElementById('backup_path').value.trim(),
backup_keep_days: parseInt(document.getElementById('backup_keep_days').value),
nfo_tmdb_api_key: document.getElementById('nfo_tmdb_api_key').value.trim() || null,
nfo_auto_create: document.getElementById('nfo_auto_create').checked,
nfo_update_on_scan: document.getElementById('nfo_update_on_scan').checked,
nfo_download_poster: document.getElementById('nfo_download_poster').checked,
nfo_download_logo: document.getElementById('nfo_download_logo').checked,
nfo_download_fanart: document.getElementById('nfo_download_fanart').checked,
nfo_image_size: document.getElementById('nfo_image_size').value
};
try { try {
const response = await fetch('/api/auth/setup', { const response = await fetch('/api/auth/setup', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(formData)
master_password: password,
anime_directory: directory
})
}); });
const data = await response.json(); const data = await response.json();
@@ -550,7 +742,7 @@
} }
// Clear message on input // Clear message on input
[passwordInput, confirmPasswordInput, directoryInput].forEach(input => { document.querySelectorAll('input, select').forEach(input => {
input.addEventListener('input', () => { input.addEventListener('input', () => {
messageContainer.innerHTML = ''; messageContainer.innerHTML = '';
}); });

View File

@@ -0,0 +1,561 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AniWorld Manager - Setup</title>
<link rel="stylesheet" href="/static/css/styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.setup-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
padding: 1rem;
}
.setup-card {
background: var(--color-surface);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
border: 1px solid var(--color-border);
}
.setup-header {
text-align: center;
margin-bottom: 2rem;
}
.setup-header .logo {
font-size: 3rem;
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.setup-header h1 {
margin: 0;
color: var(--color-text);
font-size: 1.8rem;
font-weight: 600;
}
.setup-header p {
margin: 1rem 0 0 0;
color: var(--color-text-secondary);
font-size: 1rem;
line-height: 1.5;
}
.setup-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-weight: 500;
color: var(--color-text);
font-size: 0.9rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 8px;
font-size: 1rem;
background: var(--color-background);
color: var(--color-text);
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.1);
}
.password-input-group {
position: relative;
}
.password-input {
padding-right: 3rem;
}
.password-toggle {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: color 0.2s ease;
}
.password-toggle:hover {
color: var(--color-primary);
}
.password-strength {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.strength-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
transition: background-color 0.2s ease;
}
.strength-bar.active.weak {
background: var(--color-error);
}
.strength-bar.active.fair {
background: var(--color-warning);
}
.strength-bar.active.good {
background: var(--color-info);
}
.strength-bar.active.strong {
background: var(--color-success);
}
.strength-text {
font-size: 0.8rem;
color: var(--color-text-secondary);
margin-top: 0.25rem;
}
.form-help {
font-size: 0.8rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
.setup-button {
width: 100%;
padding: 0.75rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.setup-button:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.3);
}
.setup-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.error-message {
background: var(--color-error-light);
color: var(--color-error);
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--color-error);
font-size: 0.9rem;
}
.success-message {
background: var(--color-success-light);
color: var(--color-success);
padding: 0.75rem;
border-radius: 8px;
border: 1px solid var(--color-success);
font-size: 0.9rem;
}
.security-tips {
margin-top: 1.5rem;
padding: 1rem;
background: var(--color-info-light);
border: 1px solid var(--color-info);
border-radius: 8px;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.security-tips h4 {
margin: 0 0 0.5rem 0;
color: var(--color-info);
font-size: 0.9rem;
}
.security-tips ul {
margin: 0;
padding-left: 1.2rem;
line-height: 1.4;
}
.theme-toggle {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 0.5rem;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.loading-spinner {
width: 1rem;
height: 1rem;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="setup-container">
<button class="theme-toggle" id="theme-toggle" title="Toggle theme">
<i class="fas fa-moon"></i>
</button>
<div class="setup-card">
<div class="setup-header">
<div class="logo">
<i class="fas fa-play-circle"></i>
</div>
<h1>Welcome to AniWorld Manager</h1>
<p>Let's set up your master password to secure your anime collection.</p>
</div>
<form class="setup-form" id="setup-form">
<div class="form-group">
<label for="directory" class="form-label">Anime Directory</label>
<input type="text" id="directory" name="directory" class="form-input" placeholder="C:\Anime"
value="{{ current_directory }}" required>
<div class="form-help">
The directory where your anime series are stored. This can be changed later in settings.
</div>
</div>
<div class="form-group">
<label for="password" class="form-label">Master Password</label>
<div class="password-input-group">
<input type="password" id="password" name="password" class="form-input password-input"
placeholder="Create a strong password" required minlength="8">
<button type="button" class="password-toggle" id="password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="password-strength">
<div class="strength-bar" id="strength-1"></div>
<div class="strength-bar" id="strength-2"></div>
<div class="strength-bar" id="strength-3"></div>
<div class="strength-bar" id="strength-4"></div>
</div>
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
</div>
<div class="form-group">
<label for="confirm-password" class="form-label">Confirm Password</label>
<div class="password-input-group">
<input type="password" id="confirm-password" name="confirm-password"
class="form-input password-input" placeholder="Confirm your password" required
minlength="8">
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div id="message-container"></div>
<button type="submit" class="setup-button" id="setup-button">
<i class="fas fa-check"></i>
<span>Complete Setup</span>
</button>
</form>
<div class="security-tips">
<h4><i class="fas fa-shield-alt"></i> Security Tips</h4>
<ul>
<li>Use a password with at least 12 characters</li>
<li>Include uppercase, lowercase, numbers, and symbols</li>
<li>Don't use personal information or common words</li>
<li>Consider using a password manager</li>
</ul>
</div>
</div>
</div>
<script>
// Theme toggle functionality
const themeToggle = document.getElementById('theme-toggle');
const htmlElement = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
htmlElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
htmlElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
});
function updateThemeIcon(theme) {
const icon = themeToggle.querySelector('i');
icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
// Password visibility toggles
document.querySelectorAll('.password-toggle').forEach(toggle => {
toggle.addEventListener('click', () => {
const input = toggle.parentElement.querySelector('input');
const type = input.getAttribute('type');
const newType = type === 'password' ? 'text' : 'password';
const icon = toggle.querySelector('i');
input.setAttribute('type', newType);
icon.className = newType === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash';
});
});
// Password strength checker
const passwordInput = document.getElementById('password');
const strengthBars = document.querySelectorAll('.strength-bar');
const strengthText = document.getElementById('strength-text');
passwordInput.addEventListener('input', () => {
const password = passwordInput.value;
const strength = calculatePasswordStrength(password);
updatePasswordStrength(strength);
});
function calculatePasswordStrength(password) {
let score = 0;
let feedback = [];
// Length check
if (password.length >= 8) score++;
if (password.length >= 12) score++;
// Character variety
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
// Penalties
if (password.length < 8) {
feedback.push('Too short');
score = Math.max(0, score - 2);
}
if (!/[A-Z]/.test(password)) feedback.push('Add uppercase');
if (!/[0-9]/.test(password)) feedback.push('Add numbers');
if (!/[^A-Za-z0-9]/.test(password)) feedback.push('Add symbols');
const strengthLevels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
const strengthLevel = Math.min(Math.floor(score / 1.2), 5);
return {
score: Math.min(score, 6),
level: strengthLevel,
text: strengthLevels[strengthLevel],
feedback
};
}
function updatePasswordStrength(strength) {
const colors = ['weak', 'weak', 'fair', 'good', 'strong', 'strong'];
const color = colors[strength.level];
strengthBars.forEach((bar, index) => {
bar.className = 'strength-bar';
if (index < strength.score) {
bar.classList.add('active', color);
}
});
if (passwordInput.value) {
let text = `Password strength: ${strength.text}`;
if (strength.feedback.length > 0) {
text += ` (${strength.feedback.join(', ')})`;
}
strengthText.textContent = text;
strengthText.style.color = strength.level >= 3 ? 'var(--color-success)' : 'var(--color-warning)';
} else {
strengthText.textContent = 'Password strength will be shown here';
strengthText.style.color = 'var(--color-text-secondary)';
}
}
// Form submission
const setupForm = document.getElementById('setup-form');
const setupButton = document.getElementById('setup-button');
const messageContainer = document.getElementById('message-container');
const confirmPasswordInput = document.getElementById('confirm-password');
const directoryInput = document.getElementById('directory');
// Real-time password confirmation
confirmPasswordInput.addEventListener('input', validatePasswordMatch);
passwordInput.addEventListener('input', validatePasswordMatch);
function validatePasswordMatch() {
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (confirmPassword && password !== confirmPassword) {
confirmPasswordInput.setCustomValidity('Passwords do not match');
confirmPasswordInput.style.borderColor = 'var(--color-error)';
} else {
confirmPasswordInput.setCustomValidity('');
confirmPasswordInput.style.borderColor = 'var(--color-border)';
}
}
setupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = passwordInput.value;
const confirmPassword = confirmPasswordInput.value;
const directory = directoryInput.value.trim();
if (password !== confirmPassword) {
showMessage('Passwords do not match', 'error');
return;
}
const strength = calculatePasswordStrength(password);
if (strength.level < 2) {
showMessage('Password is too weak. Please use a stronger password.', 'error');
return;
}
if (!directory) {
showMessage('Please enter a valid anime directory', 'error');
return;
}
setLoading(true);
try {
const response = await fetch('/api/auth/setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
master_password: password,
anime_directory: directory
})
});
const data = await response.json();
if (response.ok && data.status === 'ok') {
showMessage('Setup completed successfully! Redirecting to login...', 'success');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
} else {
const errorMessage = data.detail || data.message || 'Setup failed';
showMessage(errorMessage, 'error');
}
} catch (error) {
showMessage('Setup failed. Please try again.', 'error');
console.error('Setup error:', error);
} finally {
setLoading(false);
}
});
function showMessage(message, type) {
messageContainer.innerHTML = `
<div class="${type}-message">
${message}
</div>
`;
}
function setLoading(loading) {
setupButton.disabled = loading;
const buttonText = setupButton.querySelector('span');
const buttonIcon = setupButton.querySelector('i');
if (loading) {
buttonIcon.className = 'loading-spinner';
buttonText.textContent = 'Setting up...';
} else {
buttonIcon.className = 'fas fa-check';
buttonText.textContent = 'Complete Setup';
}
}
// Clear message on input
[passwordInput, confirmPasswordInput, directoryInput].forEach(input => {
input.addEventListener('input', () => {
messageContainer.innerHTML = '';
});
});
</script>
</body>
</html>