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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -110,27 +110,59 @@ For each task completed:
|
|||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,21 +308,45 @@
|
|||||||
<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">
|
||||||
|
<!-- General Settings -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
General Settings
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="directory" class="form-label">Anime Directory</label>
|
<label for="name" class="form-label">Application Name</label>
|
||||||
<input type="text" id="directory" name="directory" class="form-input" placeholder="C:\Anime"
|
<input type="text" id="name" name="name" class="form-input" value="Aniworld" required>
|
||||||
value="{{ current_directory }}" 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">
|
<div class="form-help">
|
||||||
The directory where your anime series are stored. This can be changed later in settings.
|
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="section-header">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
Security Settings
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="password" class="form-label">Master Password *</label>
|
||||||
<div class="password-input-group">
|
<div class="password-input-group">
|
||||||
<input type="password" id="password" name="password" class="form-input password-input"
|
<input type="password" id="password" name="password" class="form-input password-input"
|
||||||
placeholder="Create a strong password" required minlength="8">
|
placeholder="Create a strong password" required minlength="8">
|
||||||
@@ -308,18 +362,155 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
|
<div class="strength-text" id="strength-text">Password strength will be shown here</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
<div class="form-group">
|
<label for="confirm-password" class="form-label">Confirm Password *</label>
|
||||||
<label for="confirm-password" class="form-label">Confirm Password</label>
|
|
||||||
<div class="password-input-group">
|
<div class="password-input-group">
|
||||||
<input type="password" id="confirm-password" name="confirm-password"
|
<input type="password" id="confirm-password" name="confirm-password"
|
||||||
class="form-input password-input" placeholder="Confirm your password" required
|
class="form-input password-input" placeholder="Confirm your password" required minlength="8">
|
||||||
minlength="8">
|
|
||||||
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
|
<button type="button" class="password-toggle" id="confirm-password-toggle" tabindex="-1">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scheduler Settings -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
Scheduler Settings
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<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 id="message-container"></div>
|
<div id="message-container"></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 = '';
|
||||||
});
|
});
|
||||||
|
|||||||
561
src/server/web/templates/setup_old.html
Normal file
561
src/server/web/templates/setup_old.html
Normal 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>
|
||||||
Reference in New Issue
Block a user