feat(setup): add done button and integrate NFO scan into initialization
- Add /api/setup/unresolved/done endpoint to mark phase complete - NFO scan now runs after series sync during initialization - Middleware redirects to /login after setup complete (was /loading) - Done button allows skipping folder resolution with redirect to NFO scan phase
This commit is contained in:
@@ -13,18 +13,38 @@ The application uses a middleware-based redirect system to ensure users complete
|
|||||||
│ SETUP FLOW │
|
│ SETUP FLOW │
|
||||||
├─────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ /setup ──► /loading ──┬──► /setup/unresolved ──► /loading │
|
│ /setup ──► /loading ──► /setup/unresolved ──► /loading ──► /login │
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
│ ▼ ▼ ▼ ▼ │
|
│ ▼ ▼ ▼ ▼ │
|
||||||
│ (first time) (WebSocket) (has folders) (all resolved) │
|
│ (first (Series Scan + (has folders) (all resolved) │
|
||||||
│ │ │ │
|
│ time) NFO Scan) │ │
|
||||||
│ ▼ │ │
|
│ │ │ │
|
||||||
│ /login ◄───────────────────┴──────────────────────┤
|
│ │ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ [Done button] ──► marks complete │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ▼ │
|
||||||
|
│ │ /loading (NFO phase runs again) │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────────┴─────────────────────────────────────┘
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**New Navigation Order:**
|
||||||
|
1. `/setup` → Initial configuration
|
||||||
|
2. `/loading` → Series scan + NFO scan
|
||||||
|
3. `/setup/unresolved` → Resolve folders (if any)
|
||||||
|
4. `/loading` → NFO scan runs again
|
||||||
|
5. `/login` → Authentication
|
||||||
|
|
||||||
|
**Key Changes:**
|
||||||
|
- After `/setup/unresolved`, the "Done" button marks the phase as complete
|
||||||
|
- Revisiting `/setup/unresolved` after completion → redirects to `/loading`
|
||||||
|
- `/loading` always goes to `/setup/unresolved` if unresolved folders exist
|
||||||
|
- NFO scan runs as a separate phase after series sync during initialization
|
||||||
|
|
||||||
## Middleware: SetupRedirectMiddleware
|
## Middleware: SetupRedirectMiddleware
|
||||||
|
|
||||||
**File:** `src/server/middleware/setup_redirect.py`
|
**File:** `src/server/middleware/setup_redirect.py`
|
||||||
@@ -49,9 +69,12 @@ The middleware intercepts all requests and redirects to `/setup` if:
|
|||||||
### Middleware Logic
|
### Middleware Logic
|
||||||
|
|
||||||
1. **Setup incomplete** → Redirect to `/setup`
|
1. **Setup incomplete** → Redirect to `/setup`
|
||||||
2. **Setup complete, accessing `/setup`** → Redirect to `/loading`
|
2. **Setup complete, accessing `/setup`** → Redirect to `/login`
|
||||||
3. **Setup complete, accessing `/loading`** → Allow access (page handles its own redirect)
|
3. **Setup complete, accessing `/loading`** → Allow access (page handles its own redirect)
|
||||||
4. **API requests during setup** → Return 503 with `setup_url`
|
4. **Setup complete, accessing `/setup/unresolved`**:
|
||||||
|
- If `unresolved_completed` flag is set → Redirect to `/loading`
|
||||||
|
- Otherwise → Allow access
|
||||||
|
5. **API requests during setup** → Return 503 with `setup_url`
|
||||||
|
|
||||||
## Pages
|
## Pages
|
||||||
|
|
||||||
@@ -104,17 +127,25 @@ Allows manual resolution of folders that couldn't be auto-matched:
|
|||||||
- Provides search suggestions
|
- Provides search suggestions
|
||||||
- Input field for entering provider key
|
- Input field for entering provider key
|
||||||
- Resolve/delete actions
|
- Resolve/delete actions
|
||||||
|
- **Done button** at top to complete the phase without resolving all folders
|
||||||
|
|
||||||
**Post-resolution flow:**
|
**Post-resolution flow:**
|
||||||
```javascript
|
```javascript
|
||||||
function checkEmptyList() {
|
// After clicking "Done" button
|
||||||
if (listEl.children.length === 0) {
|
async function handleDone() {
|
||||||
// All folders resolved → return to loading
|
// Call API to mark phase as complete
|
||||||
setTimeout(() => { window.location.href = '/loading'; }, 2000);
|
await fetch('/api/setup/unresolved/done', { method: 'POST' });
|
||||||
}
|
// Redirect to loading for final NFO scan
|
||||||
|
window.location.href = '/loading';
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Done button behavior:**
|
||||||
|
- Marks all remaining folders as handled
|
||||||
|
- Sets `unresolved_completed` flag in config
|
||||||
|
- Redirects to `/loading` to run final NFO scan
|
||||||
|
- After completion, `/setup/unresolved` becomes inaccessible (redirects to `/loading`)
|
||||||
|
|
||||||
### 4. Login Page (`/login`)
|
### 4. Login Page (`/login`)
|
||||||
|
|
||||||
**File:** `src/server/web/templates/login.html`
|
**File:** `src/server/web/templates/login.html`
|
||||||
@@ -132,6 +163,7 @@ Authentication page. After successful login → redirect to `/` (main app).
|
|||||||
| `POST` | `/api/setup/unresolved/{folder_name}/resolve` | Resolve with provider key |
|
| `POST` | `/api/setup/unresolved/{folder_name}/resolve` | Resolve with provider key |
|
||||||
| `POST` | `/api/setup/unresolved/{folder_name}/search` | Re-search for matches |
|
| `POST` | `/api/setup/unresolved/{folder_name}/search` | Re-search for matches |
|
||||||
| `DELETE` | `/api/setup/unresolved/{folder_name}` | Remove folder from tracking |
|
| `DELETE` | `/api/setup/unresolved/{folder_name}` | Remove folder from tracking |
|
||||||
|
| `POST` | `/api/setup/unresolved/done` | Mark unresolved phase as complete |
|
||||||
|
|
||||||
### Auth API
|
### Auth API
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ async def setup_auth(req: SetupRequest):
|
|||||||
# Continue — scheduler failure should not break initialization
|
# Continue — scheduler failure should not break initialization
|
||||||
|
|
||||||
# Send completion event
|
# Send completion event
|
||||||
from src.server.services.progress_service import ProgressType
|
|
||||||
await progress_service.start_progress(
|
await progress_service.start_progress(
|
||||||
progress_id="initialization_complete",
|
progress_id="initialization_complete",
|
||||||
progress_type=ProgressType.SYSTEM,
|
progress_type=ProgressType.SYSTEM,
|
||||||
|
|||||||
@@ -321,4 +321,56 @@ async def delete_unresolved_folder(
|
|||||||
detail=f"Unresolved folder not found: {folder_name}"
|
detail=f"Unresolved folder not found: {folder_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "success", "message": f"Deleted unresolved folder: {folder_name}"}
|
return {"status": "success", "message": f"Deleted unresolved folder: {folder_name}"}
|
||||||
|
|
||||||
|
|
||||||
|
class DoneResponse(BaseModel):
|
||||||
|
"""Response model for completing unresolved folders."""
|
||||||
|
status: str = Field(..., description="Operation status")
|
||||||
|
message: str = Field(..., description="Human-readable message")
|
||||||
|
count: int = Field(..., description="Number of folders marked as done")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/unresolved/done", response_model=DoneResponse)
|
||||||
|
async def complete_unresolved_folders(
|
||||||
|
db=Depends(get_database_session),
|
||||||
|
) -> DoneResponse:
|
||||||
|
"""Mark all unresolved folders as handled and complete the unresolved phase.
|
||||||
|
|
||||||
|
This endpoint:
|
||||||
|
1. Marks the unresolved phase as completed in config
|
||||||
|
2. Returns the count of folders that were handled
|
||||||
|
|
||||||
|
After this, /setup/unresolved will redirect to /loading.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DoneResponse with status and count of handled folders
|
||||||
|
"""
|
||||||
|
from src.server.services.config_service import get_config_service
|
||||||
|
|
||||||
|
# Get all unresolved folders
|
||||||
|
folders = await UnresolvedFolderService.get_all_unresolved(db)
|
||||||
|
count = len(folders)
|
||||||
|
|
||||||
|
# Mark unresolved as completed in config
|
||||||
|
config_service = get_config_service()
|
||||||
|
try:
|
||||||
|
config = config_service.load_config()
|
||||||
|
if config.other is None:
|
||||||
|
config.other = {}
|
||||||
|
config.other['unresolved_completed'] = True
|
||||||
|
config_service.save_config(config, create_backup=False)
|
||||||
|
logger.info("Marked unresolved phase as completed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to save unresolved_completed flag: %s", e)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Completed unresolved phase: %d folders handled",
|
||||||
|
count
|
||||||
|
)
|
||||||
|
|
||||||
|
return DoneResponse(
|
||||||
|
status="success",
|
||||||
|
message=f"Marked {count} folders as handled. Unresolved phase completed.",
|
||||||
|
count=count,
|
||||||
|
)
|
||||||
@@ -105,6 +105,20 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _is_unresolved_completed(self) -> bool:
|
||||||
|
"""Check if the unresolved phase has been completed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if unresolved phase is complete, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_service = get_config_service()
|
||||||
|
config = config_service.load_config()
|
||||||
|
other = config.other or {}
|
||||||
|
return bool(other.get('unresolved_completed', False))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
async def dispatch(
|
async def dispatch(
|
||||||
self, request: Request, call_next: Callable
|
self, request: Request, call_next: Callable
|
||||||
) -> Response:
|
) -> Response:
|
||||||
@@ -120,13 +134,17 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
|
|||||||
path = request.url.path
|
path = request.url.path
|
||||||
|
|
||||||
# Check if trying to access setup or loading page after completion
|
# Check if trying to access setup or loading page after completion
|
||||||
if path in ("/setup", "/loading"):
|
if path in ("/setup", "/loading", "/setup/unresolved"):
|
||||||
if not self._needs_setup():
|
if not self._needs_setup():
|
||||||
# Setup is complete, check loading status
|
|
||||||
if path == "/setup":
|
if path == "/setup":
|
||||||
# Redirect to loading if initialization is in progress
|
# Redirect to loading if initialization is in progress
|
||||||
# Otherwise redirect to login
|
# Otherwise redirect to login
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
elif path == "/setup/unresolved":
|
||||||
|
# Check if unresolved phase is already completed
|
||||||
|
if self._is_unresolved_completed():
|
||||||
|
# Redirect to loading - unresolved phase already done
|
||||||
|
return RedirectResponse(url="/loading", status_code=302)
|
||||||
elif path == "/loading":
|
elif path == "/loading":
|
||||||
# Always allow access to loading page - it handles its own
|
# Always allow access to loading page - it handles its own
|
||||||
# redirect flow via WebSocket events (initialization_complete
|
# redirect flow via WebSocket events (initialization_complete
|
||||||
|
|||||||
@@ -386,6 +386,9 @@ async def perform_initial_setup(progress_service=None):
|
|||||||
# Load series into memory from database
|
# Load series into memory from database
|
||||||
await _load_series_into_memory(progress_service)
|
await _load_series_into_memory(progress_service)
|
||||||
|
|
||||||
|
# Run NFO scan as part of initialization
|
||||||
|
await perform_nfo_scan_if_needed(progress_service)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except (OSError, RuntimeError, ValueError) as e:
|
except (OSError, RuntimeError, ValueError) as e:
|
||||||
@@ -427,13 +430,46 @@ async def _is_nfo_scan_configured() -> bool:
|
|||||||
async def _execute_nfo_scan(progress_service=None) -> None:
|
async def _execute_nfo_scan(progress_service=None) -> None:
|
||||||
"""Execute the actual NFO scan with TMDB data.
|
"""Execute the actual NFO scan with TMDB data.
|
||||||
|
|
||||||
Note: NFO service removed. This function is now a no-op stub.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
progress_service: Unused. Kept to avoid breaking call-sites.
|
progress_service: Optional ProgressService for emitting updates
|
||||||
"""
|
"""
|
||||||
logger.info("NFO scan skipped — NFO service removed")
|
from src.server.services.anime_service import get_anime_service
|
||||||
return
|
from src.server.services.nfo_scan_service import NfoScanService
|
||||||
|
|
||||||
|
logger.info("Starting NFO scan...")
|
||||||
|
|
||||||
|
anime_service = get_anime_service()
|
||||||
|
nfo_service = NfoScanService()
|
||||||
|
|
||||||
|
# Subscribe to NFO events and forward to progress service
|
||||||
|
async def nfo_event_handler(event_data):
|
||||||
|
if event_data.get('type') == 'nfo_scan_progress':
|
||||||
|
data = event_data.get('data', {})
|
||||||
|
if progress_service:
|
||||||
|
await progress_service.update_progress(
|
||||||
|
progress_id="nfo_scan",
|
||||||
|
current=data.get('current', 0),
|
||||||
|
total=data.get('total', 100),
|
||||||
|
message=data.get('message', 'Scanning...'),
|
||||||
|
key=data.get('key'),
|
||||||
|
folder=data.get('folder'),
|
||||||
|
)
|
||||||
|
elif event_data.get('type') == 'nfo_scan_completed':
|
||||||
|
stats = event_data.get('statistics', {})
|
||||||
|
if progress_service:
|
||||||
|
await progress_service.complete_progress(
|
||||||
|
progress_id="nfo_scan",
|
||||||
|
message=f"NFO scan complete: {stats.get('created', 0)} created, {stats.get('updated', 0)} updated",
|
||||||
|
)
|
||||||
|
|
||||||
|
nfo_service.subscribe_to_scan_events(nfo_event_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run the scan
|
||||||
|
nfo_result = await nfo_service.scan_all(anime_service)
|
||||||
|
logger.info("NFO scan completed: %s", nfo_result)
|
||||||
|
finally:
|
||||||
|
nfo_service.unsubscribe_from_scan_events(nfo_event_handler)
|
||||||
|
|
||||||
|
|
||||||
async def perform_nfo_scan_if_needed(progress_service=None):
|
async def perform_nfo_scan_if_needed(progress_service=None):
|
||||||
@@ -446,8 +482,8 @@ async def perform_nfo_scan_if_needed(progress_service=None):
|
|||||||
from src.server.services.progress_service import ProgressType
|
from src.server.services.progress_service import ProgressType
|
||||||
await progress_service.start_progress(
|
await progress_service.start_progress(
|
||||||
progress_id="nfo_scan",
|
progress_id="nfo_scan",
|
||||||
progress_type=ProgressType.SYSTEM,
|
progress_type=ProgressType.SCAN,
|
||||||
title="Processing NFO Metadata",
|
title="Scanning NFO Files",
|
||||||
total=100,
|
total=100,
|
||||||
message="Checking NFO scan status...",
|
message="Checking NFO scan status...",
|
||||||
metadata={"step_id": "nfo_scan"}
|
metadata={"step_id": "nfo_scan"}
|
||||||
|
|||||||
@@ -281,11 +281,13 @@
|
|||||||
let isComplete = false;
|
let isComplete = false;
|
||||||
|
|
||||||
const stepOrder = [
|
const stepOrder = [
|
||||||
'series_sync'
|
'series_sync',
|
||||||
|
'nfo_scan'
|
||||||
];
|
];
|
||||||
|
|
||||||
const stepTitles = {
|
const stepTitles = {
|
||||||
'series_sync': 'Syncing Series Database'
|
'series_sync': 'Syncing Series Database',
|
||||||
|
'nfo_scan': 'Scanning NFO Files'
|
||||||
};
|
};
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
@@ -305,6 +307,14 @@
|
|||||||
room: 'system'
|
room: 'system'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Subscribe to scan room for NFO scan progress
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
action: 'join',
|
||||||
|
data: {
|
||||||
|
room: 'scan'
|
||||||
|
}
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
@@ -349,6 +359,12 @@
|
|||||||
const data = message.data || message;
|
const data = message.data || message;
|
||||||
const { type, status, title, message: msg, percent, current, total, metadata } = data;
|
const { type, status, title, message: msg, percent, current, total, metadata } = data;
|
||||||
|
|
||||||
|
// Handle NFO scan events
|
||||||
|
if (type === 'nfo_scan_started' || type === 'nfo_scan_progress' || type === 'nfo_scan_completed') {
|
||||||
|
handleNfoScanUpdate(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine step ID based on type and metadata
|
// Determine step ID based on type and metadata
|
||||||
let stepId = metadata?.step_id || type;
|
let stepId = metadata?.step_id || type;
|
||||||
|
|
||||||
@@ -370,6 +386,75 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNfoScanUpdate(data) {
|
||||||
|
const stepId = 'nfo_scan';
|
||||||
|
|
||||||
|
if (!steps.has(stepId)) {
|
||||||
|
createStep(stepId, stepTitles[stepId] || 'Scanning NFO Files');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepEl = steps.get(stepId);
|
||||||
|
if (!stepEl) return;
|
||||||
|
|
||||||
|
const iconEl = stepEl.querySelector('.step-icon');
|
||||||
|
const statusEl = stepEl.querySelector('.step-status');
|
||||||
|
const messageEl = stepEl.querySelector('.step-message');
|
||||||
|
const progressEl = stepEl.querySelector('.step-progress');
|
||||||
|
const progressFillEl = stepEl.querySelector('.progress-bar-fill');
|
||||||
|
const progressTextEl = stepEl.querySelector('.progress-text');
|
||||||
|
|
||||||
|
const nfoData = data.data || data;
|
||||||
|
const { status, message, current, total, key, folder } = nfoData;
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
stepEl.className = 'progress-step';
|
||||||
|
if (status === 'started') {
|
||||||
|
stepEl.classList.add('active');
|
||||||
|
iconEl.className = 'fas fa-circle-notch fa-spin step-icon loading';
|
||||||
|
statusEl.textContent = 'Starting...';
|
||||||
|
} else if (status === 'in_progress') {
|
||||||
|
stepEl.classList.add('active');
|
||||||
|
iconEl.className = 'fas fa-circle-notch fa-spin step-icon loading';
|
||||||
|
statusEl.textContent = 'In Progress...';
|
||||||
|
} else if (status === 'completed') {
|
||||||
|
stepEl.classList.add('completed');
|
||||||
|
iconEl.className = 'fas fa-check-circle step-icon completed';
|
||||||
|
statusEl.textContent = 'Complete';
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
stepEl.classList.add('error');
|
||||||
|
iconEl.className = 'fas fa-exclamation-circle step-icon error';
|
||||||
|
statusEl.textContent = 'Failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update message - show current folder being processed
|
||||||
|
if (message) {
|
||||||
|
messageEl.textContent = message;
|
||||||
|
messageEl.style.display = 'block';
|
||||||
|
} else if (key && folder) {
|
||||||
|
messageEl.textContent = `Processing: ${folder}`;
|
||||||
|
messageEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
if (current > 0 && total > 0) {
|
||||||
|
const actualPercent = (current / total) * 100;
|
||||||
|
progressEl.style.display = 'block';
|
||||||
|
progressFillEl.style.width = `${actualPercent}%`;
|
||||||
|
progressTextEl.textContent = `${current}/${total} series`;
|
||||||
|
} else if (percent > 0) {
|
||||||
|
progressEl.style.display = 'block';
|
||||||
|
progressFillEl.style.width = `${percent}%`;
|
||||||
|
progressTextEl.textContent = `${Math.round(percent)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completion
|
||||||
|
if (data.type === 'nfo_scan_completed') {
|
||||||
|
setTimeout(() => {
|
||||||
|
checkUnresolvedAndProceed();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createStep(stepId, title) {
|
function createStep(stepId, title) {
|
||||||
const container = document.getElementById('progressContainer');
|
const container = document.getElementById('progressContainer');
|
||||||
|
|
||||||
@@ -475,8 +560,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkUnresolvedAndProceed() {
|
async function checkUnresolvedAndProceed() {
|
||||||
// Fetch unresolved folders and only redirect if there are any
|
// Always check for unresolved folders first
|
||||||
// Otherwise go directly to login
|
// After setup -> loading, always go through unresolved if there are any
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const res = await fetch('/api/setup/unresolved', {
|
const res = await fetch('/api/setup/unresolved', {
|
||||||
@@ -493,7 +578,7 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to check unresolved folders:', err);
|
console.error('Failed to check unresolved folders:', err);
|
||||||
}
|
}
|
||||||
// No unresolved folders or error - go to login
|
// No unresolved folders - go to login
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -415,6 +415,36 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.done-btn {
|
||||||
|
background: var(--color-success);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-duration);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.done-btn:hover:not(:disabled) {
|
||||||
|
background: #27ae60;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.done-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.folder-input-row {
|
.folder-input-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -439,6 +469,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1>Resolve Unresolved Series</h1>
|
<h1>Resolve Unresolved Series</h1>
|
||||||
<p>Some series couldn't be found automatically. Enter the provider key for each folder to complete setup.</p>
|
<p>Some series couldn't be found automatically. Enter the provider key for each folder to complete setup.</p>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="done-btn" id="done-btn" onclick="handleDone()">
|
||||||
|
<i class="fas fa-check"></i> Done
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading-state" class="loading-state">
|
<div id="loading-state" class="loading-state">
|
||||||
@@ -754,6 +789,7 @@
|
|||||||
const listEl = document.getElementById('folder-list');
|
const listEl = document.getElementById('folder-list');
|
||||||
const emptyEl = document.getElementById('empty-state');
|
const emptyEl = document.getElementById('empty-state');
|
||||||
const skipLink = document.getElementById('skip-link');
|
const skipLink = document.getElementById('skip-link');
|
||||||
|
const doneBtn = document.getElementById('done-btn');
|
||||||
|
|
||||||
if (listEl.children.length === 0) {
|
if (listEl.children.length === 0) {
|
||||||
listEl.style.display = 'none';
|
listEl.style.display = 'none';
|
||||||
@@ -764,11 +800,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function completeUnresolved() {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const res = await fetch('/api/setup/unresolved/done', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDone() {
|
||||||
|
const doneBtn = document.getElementById('done-btn');
|
||||||
|
doneBtn.disabled = true;
|
||||||
|
doneBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await completeUnresolved();
|
||||||
|
if (result.status === 'success') {
|
||||||
|
showToast(result.message, 'success');
|
||||||
|
setTimeout(() => { window.location.href = '/loading'; }, 1000);
|
||||||
|
} else {
|
||||||
|
showToast(result.message || 'Failed to complete', 'error');
|
||||||
|
doneBtn.disabled = false;
|
||||||
|
doneBtn.innerHTML = '<i class="fas fa-check"></i> Done';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Server error. Please try again.', 'error');
|
||||||
|
doneBtn.disabled = false;
|
||||||
|
doneBtn.innerHTML = '<i class="fas fa-check"></i> Done';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Done button when there are folders
|
||||||
|
function showDoneButton() {
|
||||||
|
const doneBtn = document.getElementById('done-btn');
|
||||||
|
doneBtn.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
(async function init() {
|
(async function init() {
|
||||||
const folders = await fetchUnresolved();
|
const folders = await fetchUnresolved();
|
||||||
if (folders !== null) {
|
if (folders !== null) {
|
||||||
renderFolders(folders);
|
renderFolders(folders);
|
||||||
|
if (folders.length > 0) {
|
||||||
|
showDoneButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user