mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
i dont know
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
{% extends 'layouts/app.twig' %}
|
||||
{% extends 'admin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-nav me-auto">
|
||||
<a class="nav-link active" href="{{ path_for('admin.index') }}">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
|
||||
</a>
|
||||
{% if is_admin() %}
|
||||
<a class="nav-link" href="{{ path_for('admin.sources') }}">
|
||||
<i class="fas fa-database me-2"></i>Sources
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="nav-link" href="{{ path_for('admin.settings') }}">
|
||||
<i class="fas fa-cog me-2"></i>Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="display-4 fw-bold text-dark">Admin Dashboard</h1>
|
||||
<p class="lead text-muted">Manage your media sources and synchronization</p>
|
||||
|
||||
338
resources/views/admin/layout.twig
Normal file
338
resources/views/admin/layout.twig
Normal file
@@ -0,0 +1,338 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Admin Panel - MediaLib{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<!-- Custom Admin CSS -->
|
||||
<style>
|
||||
:root {
|
||||
--sidebar-width: 250px;
|
||||
--header-height: 56px;
|
||||
--primary-color: #4e73df;
|
||||
--secondary-color: #858796;
|
||||
--success-color: #1cc88a;
|
||||
--info-color: #36b9cc;
|
||||
--warning-color: #f6c23e;
|
||||
--danger-color: #e74a3b;
|
||||
--light-color: #f8f9fc;
|
||||
--dark-color: #5a5c69;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 0.9rem;
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar {
|
||||
width: var(--sidebar-width);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
height: var(--header-height);
|
||||
text-decoration: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05rem;
|
||||
z-index: 1;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-divider {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
margin: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
text-align: left;
|
||||
padding: 0 1rem;
|
||||
font-weight: 800;
|
||||
font-size: 0.65rem;
|
||||
color: var(--secondary-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.13em;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #d1d3e2;
|
||||
text-decoration: none;
|
||||
border-radius: 0.35rem;
|
||||
margin: 0 0.5rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
margin-right: 0.5rem;
|
||||
color: #b7b9cc;
|
||||
}
|
||||
|
||||
.nav-link:hover, .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-link:hover i, .nav-link.active i {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
#content {
|
||||
width: calc(100% - var(--sidebar-width));
|
||||
min-height: 100vh;
|
||||
margin-left: var(--sidebar-width);
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
/* Top Navigation */
|
||||
.topbar {
|
||||
height: var(--header-height);
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
||||
background-color: #fff;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 0.35rem;
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fc;
|
||||
border-bottom: 1px solid #e3e6f0;
|
||||
padding: 1rem 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #4e73df;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2e59d9;
|
||||
border-color: #2653d4;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom: 2px solid #e3e6f0;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #4e73df;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
margin-left: -250px;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#sidebar.active {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#content.active {
|
||||
margin-left: 250px;
|
||||
width: calc(100% - 250px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar" class="bg-dark">
|
||||
<a class="sidebar-brand" href="{{ path_for('admin.index') }}">
|
||||
<i class="bi bi-collection-play-fill me-2"></i>MediaLib Admin
|
||||
</a>
|
||||
|
||||
<div class="sidebar-divider"></div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="p-3">
|
||||
<div class="sidebar-heading">Core</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ current_route == 'admin.index' ? 'active' : '' }}" href="{{ path_for('admin.index') }}">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ current_route == 'admin.settings' ? 'active' : '' }}" href="{{ path_for('admin.settings') }}">
|
||||
<i class="bi bi-gear"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-heading mt-4">Media</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('games.index') }}" target="_blank">
|
||||
<i class="bi bi-joystick"></i>
|
||||
<span>Games</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('movies.index') }}" target="_blank">
|
||||
<i class="bi bi-film"></i>
|
||||
<span>Movies</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('tvshows.index') }}" target="_blank">
|
||||
<i class="bi bi-tv"></i>
|
||||
<span>TV Shows</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('music.index') }}" target="_blank">
|
||||
<i class="bi bi-music-note-list"></i>
|
||||
<span>Music</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-heading mt-4">System</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('admin.sources') }}">
|
||||
<i class="bi bi-hdd-rack"></i>
|
||||
<span>Media Sources</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('admin.sync') }}">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
<span>Sync Media</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ path_for('home') }}" target="_blank">
|
||||
<i bi bi-house-door"></i>
|
||||
<span>View Site</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Wrapper -->
|
||||
<div id="content">
|
||||
<!-- Top Navigation -->
|
||||
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
|
||||
<!-- Sidebar Toggle (Topbar) -->
|
||||
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<!-- Topbar Navbar -->
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<!-- Nav Item - User Information -->
|
||||
<li class="nav-item dropdown no-arrow">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="me-2 d-none d-lg-inline text-gray-600 small">
|
||||
{% if auth.check %}
|
||||
{{ auth.user.username }}
|
||||
{% else %}
|
||||
Guest
|
||||
{% endif %}
|
||||
</span>
|
||||
<i class="bi bi-person-circle" style="font-size: 1.5rem;"></i>
|
||||
</a>
|
||||
<!-- Dropdown - User Information -->
|
||||
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
|
||||
<a class="dropdown-item" href="{{ path_for('home') }}">
|
||||
<i class="bi bi-house-door me-2 text-gray-400"></i>
|
||||
Home
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{{ path_for('auth.logout') }}">
|
||||
<i class="bi bi-box-arrow-right me-2 text-gray-400"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- End of Topbar -->
|
||||
|
||||
<!-- Begin Page Content -->
|
||||
<div class="container-fluid">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<!-- /.container-fluid -->
|
||||
</div>
|
||||
<!-- End of Content Wrapper -->
|
||||
|
||||
<!-- Bootstrap core JavaScript -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom scripts -->
|
||||
<script>
|
||||
// Toggle the side navigation
|
||||
document.getElementById('sidebarToggleTop').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
document.body.classList.toggle('sidebar-toggled');
|
||||
document.getElementById('sidebar').classList.toggle('toggled');
|
||||
});
|
||||
|
||||
// Close any open menu accordions when window is resized below 768px
|
||||
window.addEventListener('resize', function() {
|
||||
if (window.innerWidth < 768) {
|
||||
document.body.classList.add('sidebar-toggled');
|
||||
document.getElementById('sidebar').classList.add('toggled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
368
resources/views/admin/settings.twig
Normal file
368
resources/views/admin/settings.twig
Normal file
@@ -0,0 +1,368 @@
|
||||
{% extends 'admin/layout.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4 rounded">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-nav me-auto">
|
||||
<a class="nav-link" href="{{ path_for('admin.index') }}">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
|
||||
</a>
|
||||
{% if is_admin() %}
|
||||
<a class="nav-link" href="{{ path_for('admin.sources') }}">
|
||||
<i class="fas fa-database me-2"></i>Sources
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="nav-link active" href="{{ path_for('admin.settings') }}">
|
||||
<i class="fas fa-cog me-2"></i>Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mb-4">
|
||||
<h1 class="display-4 fw-bold text-dark">Admin Settings</h1>
|
||||
<p class="lead text-muted">Configure your media sources and application settings</p>
|
||||
</div>
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- General Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">General Settings</h2>
|
||||
<p class="text-muted mb-0">Configure general application settings</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setting_sync_interval" class="form-label">Default Sync Interval (minutes)</label>
|
||||
<input type="number" class="form-control" id="setting_sync_interval" name="setting_sync_interval"
|
||||
value="{{ settings.sync_interval ?? 60 }}" min="5" max="1440">
|
||||
<div class="form-text">How often to check for new content (5-1440 minutes)</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setting_max_sync_items" class="form-label">Max Items per Sync</label>
|
||||
<input type="number" class="form-control" id="setting_max_sync_items" name="setting_max_sync_items"
|
||||
value="{{ settings.max_sync_items ?? 1000 }}" min="100" max="10000">
|
||||
<div class="form-text">Maximum number of items to process in a single sync operation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="setting_enable_notifications"
|
||||
name="setting_enable_notifications" {{ settings.enable_notifications ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting_enable_notifications">
|
||||
Enable sync notifications
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Send notifications when sync operations complete</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="setting_auto_cleanup"
|
||||
name="setting_auto_cleanup" {{ settings.auto_cleanup ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting_auto_cleanup">
|
||||
Auto cleanup old logs
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Automatically remove sync logs older than 30 days</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">Save General Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Source Configuration</h2>
|
||||
<p class="text-muted mb-0">Configure your media sources and their sync settings</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% for source in sources %}
|
||||
<div class="border rounded p-3 mb-4">
|
||||
<h4 class="mb-3">{{ source.display_name }}</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_api_url" class="form-label">API URL</label>
|
||||
<input type="url" class="form-control" id="sources_{{ source.id }}_api_url"
|
||||
name="sources[{{ source.id }}][api_url]" value="{{ source.api_url }}">
|
||||
<div class="form-text">Base URL for the {{ source.name }} API</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_api_key" class="form-label">API Key</label>
|
||||
<input type="password" class="form-control" id="sources_{{ source.id }}_api_key"
|
||||
name="sources[{{ source.id }}][api_key]" value="{{ source.api_key }}">
|
||||
<div class="form-text">API key for authenticating with {{ source.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source-specific configuration -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="sources_{{ source.id }}_config_username"
|
||||
name="sources[{{ source.id }}][config][username]"
|
||||
value="{{ source.config.username ?? '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_sync_type" class="form-label">Default Sync Type</label>
|
||||
<select class="form-select" id="sources_{{ source.id }}_config_sync_type"
|
||||
name="sources[{{ source.id }}][config][sync_type]">
|
||||
<option value="full" {{ (source.config.sync_type ?? 'full') == 'full' ? 'selected' : '' }}>Full Sync</option>
|
||||
<option value="incremental" {{ (source.config.sync_type ?? 'full') == 'incremental' ? 'selected' : '' }}>Incremental Sync</option>
|
||||
{% if source.name == 'jellyfin' %}
|
||||
<option value="all" {{ (source.config.sync_type ?? 'full') == 'all' ? 'selected' : '' }}>All Content</option>
|
||||
<option value="movies" {{ (source.config.sync_type ?? 'full') == 'movies' ? 'selected' : '' }}>Movies Only</option>
|
||||
<option value="tvshows" {{ (source.config.sync_type ?? 'full') == 'tvshows' ? 'selected' : '' }}>TV Shows Only</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source-specific settings based on type -->
|
||||
{% if source.name == 'jellyfin' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_libraries" class="form-label">Library IDs</label>
|
||||
<input type="text" class="form-control" id="sources_{{ source.id }}_config_libraries"
|
||||
name="sources[{{ source.id }}][config][libraries]"
|
||||
value="{{ source.config.libraries ?? '' }}"
|
||||
placeholder="e.g. 12345678-1234-1234-1234-123456789012">
|
||||
<div class="form-text">Comma-separated list of Jellyfin library IDs to sync</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_user_id" class="form-label">User ID</label>
|
||||
<input type="text" class="form-control" id="sources_{{ source.id }}_config_user_id"
|
||||
name="sources[{{ source.id }}][config][user_id]"
|
||||
value="{{ source.config.user_id ?? '' }}"
|
||||
placeholder="e.g. 12345678-1234-1234-1234-123456789012">
|
||||
<div class="form-text">Jellyfin user ID for accessing content</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if source.name == 'xbvr' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_quality_filter" class="form-label">Quality Filter</label>
|
||||
<select class="form-select" id="sources_{{ source.id }}_config_quality_filter"
|
||||
name="sources[{{ source.id }}][config][quality_filter]">
|
||||
<option value="">All Qualities</option>
|
||||
<option value="4k" {{ (source.config.quality_filter ?? '') == '4k' ? 'selected' : '' }}>4K Only</option>
|
||||
<option value="1080p" {{ (source.config.quality_filter ?? '') == '1080p' ? 'selected' : '' }}>1080p Only</option>
|
||||
<option value="720p" {{ (source.config.quality_filter ?? '') == '720p' ? 'selected' : '' }}>720p Only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_rating_filter" class="form-label">Minimum Rating</label>
|
||||
<select class="form-select" id="sources_{{ source.id }}_config_rating_filter"
|
||||
name="sources[{{ source.id }}][config][rating_filter]">
|
||||
<option value="">No Filter</option>
|
||||
<option value="3" {{ (source.config.rating_filter ?? '') == '3' ? 'selected' : '' }}>3+ Stars</option>
|
||||
<option value="4" {{ (source.config.rating_filter ?? '') == '4' ? 'selected' : '' }}>4+ Stars</option>
|
||||
<option value="5" {{ (source.config.rating_filter ?? '') == '5' ? 'selected' : '' }}>5 Stars Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if source.name == 'stash' %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_include_tags" class="form-label">Include Tags</label>
|
||||
<input type="text" class="form-control" id="sources_{{ source.id }}_config_include_tags"
|
||||
name="sources[{{ source.id }}][config][include_tags]"
|
||||
value="{{ source.config.include_tags ?? '' }}"
|
||||
placeholder="e.g. VR,4K,Favorite">
|
||||
<div class="form-text">Comma-separated list of tags to include (empty for all)</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sources_{{ source.id }}_config_exclude_tags" class="form-label">Exclude Tags</label>
|
||||
<input type="text" class="form-control" id="sources_{{ source.id }}_config_exclude_tags"
|
||||
name="sources[{{ source.id }}][config][exclude_tags]"
|
||||
value="{{ source.config.exclude_tags ?? '' }}"
|
||||
placeholder="e.g. Low Quality,Test">
|
||||
<div class="form-text">Comma-separated list of tags to exclude</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="sources_{{ source.id }}_is_active"
|
||||
name="sources[{{ source.id }}][is_active]" {{ source.is_active ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="sources_{{ source.id }}_is_active">
|
||||
Enable this source for syncing
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">Save Source Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Type Visibility Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Media Type Visibility</h2>
|
||||
<p class="text-muted mb-0">Control which media types are visible to non-authenticated users</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_games" class="form-label">Games</label>
|
||||
<select class="form-select" id="media_visibility_games" name="media_visibility[games]">
|
||||
<option value="public" {{ (settings.media_visibility.games ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.games ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.games ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_movies" class="form-label">Movies</label>
|
||||
<select class="form-select" id="media_visibility_movies" name="media_visibility[movies]">
|
||||
<option value="public" {{ (settings.media_visibility.movies ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.movies ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.movies ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_tvshows" class="form-label">TV Shows</label>
|
||||
<select class="form-select" id="media_visibility_tvshows" name="media_visibility[tvshows]">
|
||||
<option value="public" {{ (settings.media_visibility.tvshows ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.tvshows ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.tvshows ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_music" class="form-label">Music</label>
|
||||
<select class="form-select" id="media_visibility_music" name="media_visibility[music]">
|
||||
<option value="public" {{ (settings.media_visibility.music ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.music ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.music ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_adult" class="form-label">Adult Videos</label>
|
||||
<select class="form-select" id="media_visibility_adult" name="media_visibility[adult]">
|
||||
<option value="public" {{ (settings.media_visibility.adult ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.adult ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.adult ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
<div class="form-text">Adult content should typically require authentication</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="media_visibility_actors" class="form-label">Actors/Performers</label>
|
||||
<select class="form-select" id="media_visibility_actors" name="media_visibility[actors]">
|
||||
<option value="public" {{ (settings.media_visibility.actors ?? 'authenticated') == 'public' ? 'selected' : '' }}>Visible to everyone</option>
|
||||
<option value="authenticated" {{ (settings.media_visibility.actors ?? 'authenticated') == 'authenticated' ? 'selected' : '' }}>Authenticated users only</option>
|
||||
<option value="hidden" {{ (settings.media_visibility.actors ?? 'authenticated') == 'hidden' ? 'selected' : '' }}>Hidden from all users</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">Save Media Visibility Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Security Settings</h2>
|
||||
<p class="text-muted mb-0">Configure security and access settings</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setting_session_timeout" class="form-label">Session Timeout (minutes)</label>
|
||||
<input type="number" class="form-control" id="setting_session_timeout" name="setting_session_timeout"
|
||||
value="{{ settings.session_timeout ?? 30 }}" min="5" max="480">
|
||||
<div class="form-text">User sessions will expire after this many minutes of inactivity</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="setting_max_login_attempts" class="form-label">Max Login Attempts</label>
|
||||
<input type="number" class="form-control" id="setting_max_login_attempts" name="setting_max_login_attempts"
|
||||
value="{{ settings.max_login_attempts ?? 5 }}" min="3" max="10">
|
||||
<div class="form-text">Number of failed login attempts before account lockout</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="setting_require_https"
|
||||
name="setting_require_https" {{ settings.require_https ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting_require_https">
|
||||
Require HTTPS
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Force all connections to use HTTPS</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="setting_enable_audit_log"
|
||||
name="setting_enable_audit_log" {{ settings.enable_audit_log ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="setting_enable_audit_log">
|
||||
Enable audit logging
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Log all admin actions for security auditing</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary">Save Security Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
205
resources/views/admin/sources.twig
Normal file
205
resources/views/admin/sources.twig
Normal file
@@ -0,0 +1,205 @@
|
||||
{% extends 'admin/layout.twig' %}
|
||||
|
||||
{% block title %}Media Sources - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">Media Sources</h1>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSourceModal">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Source
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if sources is not empty %}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Path/URL</th>
|
||||
<th>Status</th>
|
||||
<th>Last Sync</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for source in sources %}
|
||||
<tr>
|
||||
<td>{{ source.name }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ source.type == 'jellyfin' ? 'info' : 'success' }}">
|
||||
{{ source.type|upper }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ source.path }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ source.is_active ? 'success' : 'secondary' }}">
|
||||
{{ source.is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ source.last_sync ? source.last_sync|date('Y-m-d H:i:s') : 'Never' }}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Edit"
|
||||
onclick="editSource({{ source|json_encode|e('js') }});">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<a href="{{ path_for('admin.sync', {'id': source.id}) }}"
|
||||
class="btn btn-outline-success"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Sync Now">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-danger"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Delete"
|
||||
onclick="confirmDelete({{ source.id }}, '{{ source.name }}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-hdd-rack display-1 text-muted mb-4"></i>
|
||||
<h3>No Media Sources Found</h3>
|
||||
<p class="text-muted">Add your first media source to get started.</p>
|
||||
<button type="button" class="btn btn-primary mt-3" data-bs-toggle="modal" data-bs-target="#addSourceModal">
|
||||
<i class="bi bi-plus-circle me-2"></i>Add Media Source
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Add/Edit Source Modal -->
|
||||
<div class="modal fade" id="sourceModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="sourceForm" method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Add Media Source</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="id" id="sourceId">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">Type</label>
|
||||
<select class="form-select" id="type" name="type" required>
|
||||
<option value="jellyfin">Jellyfin</option>
|
||||
<option value="local">Local Filesystem</option>
|
||||
<option value="samba">Samba Share</option>
|
||||
<option value="nfs">NFS Share</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="path" class="form-label">Path/URL</label>
|
||||
<input type="text" class="form-control" id="path" name="path" required>
|
||||
<div class="form-text">
|
||||
For Jellyfin: http(s)://server:port<br>
|
||||
For local: /path/to/media<br>
|
||||
For network shares: //server/share or nfs://server/path
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username (if required)</label>
|
||||
<input type="text" class="form-control" id="username" name="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password (if required)</label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="isActive" name="is_active" checked>
|
||||
<label class="form-check-label" for="isActive">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirm Deletion</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong id="sourceName"></strong>?</p>
|
||||
<p class="text-danger">This action cannot be undone and will remove all associated media data.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form id="deleteForm" method="post" action="">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
});
|
||||
|
||||
// Handle add source button
|
||||
document.getElementById('addSourceBtn').addEventListener('click', function() {
|
||||
document.getElementById('modalTitle').textContent = 'Add Media Source';
|
||||
document.getElementById('sourceForm').reset();
|
||||
document.getElementById('sourceId').value = '';
|
||||
document.getElementById('sourceForm').action = '{{ path_for("admin.sources.store") }}';
|
||||
var modal = new bootstrap.Modal(document.getElementById('sourceModal'));
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Handle edit source
|
||||
function editSource(source) {
|
||||
document.getElementById('modalTitle').textContent = 'Edit Media Source';
|
||||
document.getElementById('sourceId').value = source.id;
|
||||
document.getElementById('name').value = source.name;
|
||||
document.getElementById('type').value = source.type;
|
||||
document.getElementById('path').value = source.path;
|
||||
document.getElementById('username').value = source.username || '';
|
||||
document.getElementById('isActive').checked = source.is_active;
|
||||
document.getElementById('sourceForm').action = '{{ path_for("admin.sources.update", {id: 0}) }}'.replace('/0', '/' + source.id);
|
||||
|
||||
var modal = new bootstrap.Modal(document.getElementById('sourceModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Handle delete confirmation
|
||||
function confirmDelete(id, name) {
|
||||
document.getElementById('sourceName').textContent = name;
|
||||
document.getElementById('deleteForm').action = '{{ path_for("admin.sources.destroy", {id: 0}) }}'.replace('/0', '/' + id);
|
||||
var modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
453
resources/views/admin/sync.twig
Normal file
453
resources/views/admin/sync.twig
Normal file
@@ -0,0 +1,453 @@
|
||||
{% extends 'admin/layout.twig' %}
|
||||
|
||||
{% block title %}Sync Media - Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">Sync Media</h1>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" id="runFullSync">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Sync Status</h6>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="syncOptions" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-gear me-1"></i>Options
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="syncOptions">
|
||||
<li><a class="dropdown-item" href="#" id="clearCompleted">Clear Completed</a></li>
|
||||
<li><a class="dropdown-item" href="#" id="clearAll">Clear All</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" id="refreshLogs">Refresh</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="syncLogs" class="log-container">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-arrow-repeat display-4"></i>
|
||||
<p class="mt-2">No sync activity yet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Sync Statistics</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-4">
|
||||
<div class="position-relative d-inline-block">
|
||||
<div id="syncProgress" class="progress-circle" data-value="0">
|
||||
<span class="progress-left">
|
||||
<span class="progress-bar"></span>
|
||||
</span>
|
||||
<span class="progress-right">
|
||||
<span class="progress-bar"></span>
|
||||
</span>
|
||||
<div class="progress-value w-100 h-100 rounded-circle d-flex align-items-center justify-content-center">
|
||||
<div class="h2 font-weight-bold">0<small>%</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="mt-3" id="syncStatus">Idle</h5>
|
||||
<p class="text-muted mb-0">Last sync: <span id="lastSyncTime">Never</span></p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span>Movies</span>
|
||||
<span id="movieCount">0</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div id="movieProgress" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span>TV Shows</span>
|
||||
<span id="tvShowCount">0</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div id="tvShowProgress" class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span>Music</span>
|
||||
<span id="musicCount">0</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div id="musicProgress" class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Quick Actions</h6>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" id="scanLibraries">
|
||||
<span><i class="bi bi-search me-2"></i> Scan Libraries</span>
|
||||
<span class="badge bg-primary rounded-pill" id="pendingScans">0</span>
|
||||
</button>
|
||||
<button class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i> Update Metadata
|
||||
</button>
|
||||
<button class="list-group-item list-group-item-action">
|
||||
<i class="bi bi-images me-2"></i> Refresh Images
|
||||
</button>
|
||||
<button class="list-group-item list-group-item-action text-danger" id="cancelSync">
|
||||
<i class="bi bi-x-circle me-2"></i> Cancel Sync
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.log-container {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #2d2d2d;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #6c757d;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #4e73df;
|
||||
}
|
||||
|
||||
.log-success {
|
||||
color: #1cc88a;
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: #f6c23e;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #e74a3b;
|
||||
}
|
||||
|
||||
/* Circular progress */
|
||||
.progress-circle {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-circle .progress-left,
|
||||
.progress-circle .progress-right {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 10px solid #f8f9fc;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.progress-circle .progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border-width: 10px;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
.progress-circle .progress-left .progress-bar {
|
||||
left: 100%;
|
||||
border-top-right-radius: 80px;
|
||||
border-bottom-right-radius: 80px;
|
||||
border-left: 0;
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
.progress-circle .progress-right .progress-bar {
|
||||
left: -100%;
|
||||
border-top-left-radius: 80px;
|
||||
border-bottom-left-radius: 80px;
|
||||
border-right: 0;
|
||||
transform-origin: center right;
|
||||
}
|
||||
|
||||
.progress-circle .progress-value {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0.15rem 1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Handle run full sync
|
||||
$('#runFullSync').click(function() {
|
||||
if (confirm('Are you sure you want to run a full sync? This may take a while.')) {
|
||||
startSync('full');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle scan libraries
|
||||
$('#scanLibraries').click(function() {
|
||||
startSync('scan');
|
||||
});
|
||||
|
||||
// Handle cancel sync
|
||||
$('#cancelSync').click(function() {
|
||||
if (confirm('Are you sure you want to cancel the current sync?')) {
|
||||
cancelSync();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle clear logs
|
||||
$('#clearCompleted').click(function(e) {
|
||||
e.preventDefault();
|
||||
clearLogs('completed');
|
||||
});
|
||||
|
||||
$('#clearAll').click(function(e) {
|
||||
e.preventDefault();
|
||||
clearLogs('all');
|
||||
});
|
||||
|
||||
// Handle refresh logs
|
||||
$('#refreshLogs').click(function(e) {
|
||||
e.preventDefault();
|
||||
loadSyncStatus();
|
||||
});
|
||||
|
||||
// Function to start sync
|
||||
function startSync(type) {
|
||||
// Show loading state
|
||||
$('#runFullSync').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
||||
|
||||
// Make AJAX request to start sync
|
||||
$.post('{{ path_for("admin.sync.start") }}', { type: type }, function(response) {
|
||||
if (response.success) {
|
||||
addLog('Sync started successfully', 'success');
|
||||
// Start polling for updates
|
||||
pollSyncStatus();
|
||||
} else {
|
||||
addLog('Error starting sync: ' + (response.message || 'Unknown error'), 'error');
|
||||
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
||||
}
|
||||
}).fail(function() {
|
||||
addLog('Failed to start sync. Please try again.', 'error');
|
||||
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to cancel sync
|
||||
function cancelSync() {
|
||||
$.post('{{ path_for("admin.sync.cancel") }}', function(response) {
|
||||
if (response.success) {
|
||||
addLog('Sync cancelled', 'warning');
|
||||
} else {
|
||||
addLog('Error cancelling sync: ' + (response.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}).fail(function() {
|
||||
addLog('Failed to cancel sync. Please try again.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Function to clear logs
|
||||
function clearLogs(type) {
|
||||
// Make AJAX request to clear logs
|
||||
$.post('{{ path_for("admin.sync.clear-logs") }}', { type: type }, function(response) {
|
||||
if (response.success) {
|
||||
if (type === 'all') {
|
||||
$('#syncLogs').html('<div class="text-center text-muted py-4"><i class="bi bi-arrow-repeat display-4"></i><p class="mt-2">No sync activity yet</p></div>');
|
||||
} else {
|
||||
$('.log-entry.completed').remove();
|
||||
if ($('.log-entry').length === 0) {
|
||||
$('#syncLogs').html('<div class="text-center text-muted py-4"><i class="bi bi-arrow-repeat display-4"></i><p class="mt-2">No sync activity yet</p></div>');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add log entry
|
||||
function addLog(message, type = 'info') {
|
||||
let logContainer = $('#syncLogs');
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
|
||||
// Remove empty state if present
|
||||
if (logContainer.find('.text-center').length) {
|
||||
logContainer.empty();
|
||||
}
|
||||
|
||||
// Create log entry
|
||||
const logEntry = $(`
|
||||
<div class="log-entry">
|
||||
<span class="log-time">[${timeString}]</span>
|
||||
<span class="log-${type}">${message}</span>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Add to container and scroll to bottom
|
||||
logContainer.append(logEntry);
|
||||
logContainer.scrollTop(logContainer[0].scrollHeight);
|
||||
}
|
||||
|
||||
// Function to update progress
|
||||
function updateProgress(progress) {
|
||||
// Update circular progress
|
||||
const progressValue = progress.percent || 0;
|
||||
const $progressCircle = $('.progress-circle');
|
||||
|
||||
// Animate progress circle
|
||||
if (progressValue > 0) {
|
||||
const $leftCircle = $progressCircle.find('.progress-left .progress-bar');
|
||||
const $rightCircle = $progressCircle.find('.progress-right .progress-bar');
|
||||
|
||||
if (progressValue <= 50) {
|
||||
$rightCircle.css('transform', `rotate(${progressValue * 3.6}deg)`);
|
||||
$leftCircle.css('transform', 'rotate(0deg)');
|
||||
} else {
|
||||
$rightCircle.css('transform', 'rotate(180deg)');
|
||||
$leftCircle.css('transform', `rotate(${(progressValue - 50) * 3.6}deg)`);
|
||||
}
|
||||
|
||||
$progressCircle.find('.progress-value div').text(`${Math.round(progressValue)}%`);
|
||||
}
|
||||
|
||||
// Update status text
|
||||
if (progress.status) {
|
||||
$('#syncStatus').text(progress.status);
|
||||
}
|
||||
|
||||
// Update last sync time
|
||||
if (progress.lastSync) {
|
||||
$('#lastSyncTime').text(progress.lastSync);
|
||||
}
|
||||
|
||||
// Update media type counts and progress
|
||||
if (progress.movies) {
|
||||
$('#movieCount').text(progress.movies.processed + ' / ' + progress.movies.total);
|
||||
const moviePercent = progress.movies.total > 0 ? Math.round((progress.movies.processed / progress.movies.total) * 100) : 0;
|
||||
$('#movieProgress').css('width', moviePercent + '%');
|
||||
}
|
||||
|
||||
if (progress.tvShows) {
|
||||
$('#tvShowCount').text(progress.tvShows.processed + ' / ' + progress.tvShows.total);
|
||||
const tvShowPercent = progress.tvShows.total > 0 ? Math.round((progress.tvShows.processed / progress.tvShows.total) * 100) : 0;
|
||||
$('#tvShowProgress').css('width', tvShowPercent + '%');
|
||||
}
|
||||
|
||||
if (progress.music) {
|
||||
$('#musicCount').text(progress.music.processed + ' / ' + progress.music.total);
|
||||
const musicPercent = progress.music.total > 0 ? Math.round((progress.music.processed / progress.music.total) * 100) : 0;
|
||||
$('#musicProgress').css('width', musicPercent + '%');
|
||||
}
|
||||
|
||||
// Update sync button state
|
||||
if (progress.isRunning) {
|
||||
$('#runFullSync').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Syncing...');
|
||||
$('#cancelSync').prop('disabled', false);
|
||||
} else {
|
||||
$('#runFullSync').prop('disabled', false).html('<i class="bi bi-arrow-repeat me-2"></i>Run Full Sync');
|
||||
$('#cancelSync').prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to poll for sync status
|
||||
function pollSyncStatus() {
|
||||
loadSyncStatus();
|
||||
|
||||
// Only continue polling if sync is in progress
|
||||
if ($('#syncStatus').text() === 'In Progress') {
|
||||
setTimeout(pollSyncStatus, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load sync status
|
||||
function loadSyncStatus() {
|
||||
$.get('{{ path_for("admin.sync.status") }}', function(response) {
|
||||
if (response.success) {
|
||||
updateProgress(response.progress);
|
||||
|
||||
// Update logs if available
|
||||
if (response.logs && response.logs.length > 0) {
|
||||
const $logContainer = $('#syncLogs');
|
||||
$logContainer.empty();
|
||||
|
||||
response.logs.forEach(log => {
|
||||
const logEntry = $(`
|
||||
<div class="log-entry ${log.status === 'completed' ? 'completed' : ''}">
|
||||
<span class="log-time">[${log.time}]</span>
|
||||
<span class="log-${log.type}">${log.message}</span>
|
||||
</div>
|
||||
`);
|
||||
$logContainer.append(logEntry);
|
||||
});
|
||||
|
||||
$logContainer.scrollTop($logContainer[0].scrollHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadSyncStatus();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user