v0.1.0
This commit is contained in:
20
templates/admin.html
Normal file
20
templates/admin.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark border-bottom">
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="/admin/link_issues">Link Issues
|
||||
{% include 'partials/_unlinked_tracks_badge.html' %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/tasks">Tasks</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{% block admin_content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
8
templates/admin/link_issues.html
Normal file
8
templates/admin/link_issues.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "admin.html" %}
|
||||
{% block admin_content %}
|
||||
<h4>Resolve Issues which occured during the linking between a spotify track and jellyfin track</h4>
|
||||
|
||||
|
||||
{% include 'partials/_track_table.html' %}
|
||||
|
||||
{% endblock %}
|
||||
21
templates/admin/tasks.html
Normal file
21
templates/admin/tasks.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "admin.html" %}
|
||||
{% block admin_content %}
|
||||
<div class="container mt-5">
|
||||
|
||||
<!-- Tabelle für den Task-Status -->
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task Name</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- Das Partial wird dynamisch über HTMX geladen -->
|
||||
<tbody id="task-status" hx-get="/task_status" hx-trigger="every 1s" hx-swap="innerHTML">
|
||||
{% include 'partials/_task_status.html' %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
171
templates/base.html
Normal file
171
templates/base.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
<!-- Icons (optional) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||
integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
|
||||
|
||||
<script src="https://unpkg.com/htmx.org"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Sidebar container with static sidebar for larger screens and offcanvas for mobile -->
|
||||
<div class="d-flex">
|
||||
<!-- Offcanvas sidebar for mobile screens -->
|
||||
<div class="offcanvas offcanvas-start d-md-none" tabindex="-1" id="mobileSidebar"
|
||||
aria-labelledby="mobileSidebarLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="mobileSidebarLabel">Jellyplist</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<nav>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/playlists"><i class="fab fa-spotify"></i> Featured</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/categories"><i class="fa-solid fa-layer-group"></i>
|
||||
Categories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/playlists/monitored"><i
|
||||
class="fa-solid fa-tower-observation"></i> Monitored</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/jellyfin_playlists"><i class="fas fa-list"></i> Jellyfin
|
||||
Playlists</a>
|
||||
</li>
|
||||
{% if session.get('is_admin') and session.get('debug') %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin"><i class="fas fa-flask"></i> Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logout"><i class="fas fa-sign-in-alt"></i> Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar for larger screens -->
|
||||
<div class="d-none d-md-block sidebar p-4 vh-100 sticky-top">
|
||||
<nav>
|
||||
<div class="sidebar-logo mb-4 text-center text-white">
|
||||
<img src="/static/images/logo_large.png" alt="Logo">
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/playlists"><i class="fab fa-spotify"></i> Featured</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/categories"><i class="fa-solid fa-layer-group"></i>
|
||||
Categories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/playlists/monitored"><i
|
||||
class="fa-solid fa-tower-observation"></i> Monitored </a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/jellyfin_playlists"><i class="fas fa-list"></i> Jellyfin
|
||||
Playlists</a>
|
||||
</li>
|
||||
{% if session.get('is_admin') %}
|
||||
<li class="nav-item">
|
||||
|
||||
<a class="nav-link text-white" href="/admin"><i class="fas fa-flask"></i> Admin
|
||||
{% include 'partials/_unlinked_tracks_badge.html' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/logout"><i class="fas fa-sign-in-alt"></i> Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<span class="fixed-bottom m-3">{{version}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Main content with toggle button for mobile sidebar -->
|
||||
<div class="container-fluid p-4 ">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<!-- Toggle button for mobile sidebar -->
|
||||
<button class="btn btn-primary d-md-none" type="button" data-bs-toggle="offcanvas"
|
||||
data-bs-target="#mobileSidebar" aria-controls="mobileSidebar">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<h1 class="mb-4 ms-3">{{ title }}</h1>
|
||||
|
||||
<div class="d-flex align-items-center ">
|
||||
<form action="/search" method="GET" class="w-100">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control"
|
||||
name="query"
|
||||
placeholder="Search Spotify..."
|
||||
aria-label="Search"
|
||||
>
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ms-4">
|
||||
<!-- Display Initials Badge -->
|
||||
<span >{{ session.get('jellyfin_user_name') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div id="alerts"></div>
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("showToastMessages", function () {
|
||||
console.log("showToastMessages")
|
||||
var toastElements = document.querySelectorAll('.toast');
|
||||
console.log(toastElements)
|
||||
|
||||
toastElements.forEach(function (toastNode) {
|
||||
var toast = new bootstrap.Toast(toastNode);
|
||||
console.log(toast);
|
||||
|
||||
toast.show();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:afterSwap", (event) => {
|
||||
console.log("htmx.afterswap")
|
||||
var toastElements = document.querySelectorAll('.toast');
|
||||
console.log(toastElements)
|
||||
toastElements.forEach(function (toastNode) {
|
||||
var toast = new bootstrap.Toast(toastNode);
|
||||
console.log(toast);
|
||||
toast.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/preview.js"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6
templates/index.html
Normal file
6
templates/index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
12
templates/items.html
Normal file
12
templates/items.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">{{ items_title }}</h1>
|
||||
<h6 class="mb-4">{{ items_subtitle }}</h6>
|
||||
<div class="row row-cols-1 row-cols-md-4 row-cols-lg-6 g-4" id="items-container">
|
||||
{% include 'partials/_spotify_items.html' %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
15
templates/jellyfin_playlists.html
Normal file
15
templates/jellyfin_playlists.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 >Your subscribed Jellyfin Playlists</h1>
|
||||
<h6 ></h6>
|
||||
<div class="row row-cols-1 row-cols-md-4 row-cols-lg-6 g-4" id="items-container">
|
||||
{% for item in playlists %}
|
||||
{% include 'partials/_spotify_item.html' %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
55
templates/login.html
Normal file
55
templates/login.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jellyplist Login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
||||
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="vh-100" >
|
||||
<div class="container py-5 h-100">
|
||||
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow " >
|
||||
<div class="card-header logo">
|
||||
<img src="/static/images/logo_large.png" alt="Logo">
|
||||
|
||||
</div>
|
||||
<div class="card-body p-5 text-center">
|
||||
<h4 class="mb-5">Login using your Jellyfin Credentials</h4>
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-4">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control form-control-lg" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control form-control-lg" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg">Login</button>
|
||||
|
||||
</form>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
{{ messages[0][1] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
12
templates/partials/_add_remove_button.html
Normal file
12
templates/partials/_add_remove_button.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% if item.can_add %}
|
||||
<button class="btn btn-success" hx-post="/addplaylist" hx-include="this" hx-swap="outerHTML" hx-target="this"
|
||||
data-bs-toggle="tooltip" title="Add to my Jellyfin"
|
||||
hx-vals='{"item_id": "{{ item.id }}", "item_name": "{{ item.name }}"}'>
|
||||
<i class="fa-solid fa-circle-plus"> </i>
|
||||
</button>
|
||||
{% elif item.can_remove %}
|
||||
<button class="btn btn-warning" hx-delete="{{ url_for('delete_playlist', playlist_id=item['jellyfin_id']) }}"
|
||||
hx-include="this" hx-swap="outerHTML" hx-target="this" data-bs-toggle="tooltip" title="Remove from Jellyfin">
|
||||
<i class="fa-solid fa-circle-minus"> </i>
|
||||
</button>
|
||||
{% endif %}
|
||||
37
templates/partials/_jf_search_results.html
Normal file
37
templates/partials/_jf_search_results.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<div id="empty"></div>
|
||||
<div class="search-results">
|
||||
{% if results %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Artist(s)</th>
|
||||
<th>Path</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in results %}
|
||||
<tr>
|
||||
<td>{{ track.Name }}</td>
|
||||
<td>{{ ', '.join(track.Artists) }}</td>
|
||||
<td>{{ track.Path}}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="playJellyfinTrack(this, '{{ track.Id }}')">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button hx-swap="beforebegin" class="btn btn-sm btn-success" hx-post="/associate_track" hx-vals='{"jellyfin_id": "{{ track.Id }}","spotify_id": "{{ spotify_id}}"}'>
|
||||
Associate Track
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No results found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
14
templates/partials/_playlist_info.html
Normal file
14
templates/partials/_playlist_info.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="d-flex align-items-center row sticky-top py-3 mb-3 bg-dark" style="top: 0; z-index: 1000;">
|
||||
<div class="col-6">
|
||||
<img src="{{ playlist_cover }}" class="figure-img">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="playlist-info">
|
||||
<h1>{{ playlist_name }}</h1>
|
||||
<p>{{ playlist_description }}</p>
|
||||
<p>{{ track_count }} songs, {{ total_duration }}</p>
|
||||
<p>Last Updated: {{ last_updated}} | Last Change: {{ last_changed}}</p>
|
||||
{% include 'partials/_add_remove_button.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
31
templates/partials/_searchresults.html
Normal file
31
templates/partials/_searchresults.html
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
|
||||
{% if playlists %}
|
||||
<h1 class="mb-4">Playlists</h1>
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
{% for item in playlists %}
|
||||
{% include 'partials/_spotify_item.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if albums %}
|
||||
<h1 class="mb-4">Albums</h1>
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
|
||||
{% for item in albums %}
|
||||
{% include 'partials/_spotify_item.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if artists %}
|
||||
<h1 class="mb-4">Artists</h1>
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
|
||||
{% for item in artists %}
|
||||
{% include 'partials/_spotify_item.html' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
49
templates/partials/_spotify_item.html
Normal file
49
templates/partials/_spotify_item.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<div class="col">
|
||||
<div class="card h-100 d-flex flex-column position-relative">
|
||||
|
||||
<!-- Badge: Only show if status is available (i.e., playlist has been requested) -->
|
||||
{% if item.status %}
|
||||
<span style="z-index: 99;" class="badge position-absolute top-0 end-0 m-2
|
||||
{% if item.status == 'green' %} bg-success
|
||||
{% elif item.status == 'yellow' %} bg-warning text-dark
|
||||
{% else %} bg-danger {% endif %}" data-bs-toggle="tooltip" title="{% if item.track_count > 0 %}
|
||||
{{ item.tracks_available }} Track Available / {{ item.tracks_linked}} Tracks linked/ {{ item.track_count}} Total
|
||||
{%endif%}
|
||||
">
|
||||
{% if item.track_count > 0 %}
|
||||
{{ item.tracks_available }} / {{ item.tracks_linked}} / {{ item.track_count}}
|
||||
{% else %}
|
||||
not Available
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Card Image -->
|
||||
<div style="position: relative;">
|
||||
<img src="{{ item.image }}" class="card-img-top" alt="{{ item.name }}">
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<h5 class="card-title">{{ item.name }}</h5>
|
||||
<p class="card-text">{{ item.description }}</p>
|
||||
</div>
|
||||
<div class="mt-auto pt-3">
|
||||
{% if item.type == 'category'%}
|
||||
<a href="{{ item.url }}" class="btn btn-primary" data-bs-toggle="tooltip" title="View Playlists">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
{%else%}
|
||||
|
||||
<a href="/playlist/view/{{ item.id }}" class="btn btn-primary" data-bs-toggle="tooltip"
|
||||
title="View Playlist details">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</a>
|
||||
{%endif%}
|
||||
{% include 'partials/_add_remove_button.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
24
templates/partials/_spotify_items.html
Normal file
24
templates/partials/_spotify_items.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% for item in items %}
|
||||
{% include 'partials/_spotify_item.html' %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if next_offset < total_items %}
|
||||
<div hx-get="{{ endpoint }}?offset={{ next_offset }}{{ additional_query }}"
|
||||
hx-trigger="revealed"
|
||||
hx-swap="beforeend"
|
||||
hx-indicator=".loading-indicator"
|
||||
hx-target="#items-container"
|
||||
class="loading-indicator text-center">
|
||||
Loading more items...
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
// Show the loading indicator only when it is active
|
||||
document.querySelectorAll('.loading-indicator').forEach(indicator => {
|
||||
indicator.addEventListener('htmx:afterRequest', () => {
|
||||
indicator.style.display = 'none'; // Hide the indicator after the request completes
|
||||
});
|
||||
});
|
||||
</script>
|
||||
22
templates/partials/_task_status.html
Normal file
22
templates/partials/_task_status.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% for task_name, task in tasks.items() %}
|
||||
<tr id="task-row-{{ task_name }}">
|
||||
<td>{{ task_name }}</td>
|
||||
<td>{{ task.state }}</td>
|
||||
<td>
|
||||
{% if task.info.percent %}
|
||||
{{ task.info.percent }}%
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
|
||||
<button hx-post="/run_task/{{ task_name }}" hx-target="#task-row-{{ task_name }}" hx-swap="outerHTML"
|
||||
class="btn btn-primary">
|
||||
Run Task
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
18
templates/partials/_toast.html
Normal file
18
templates/partials/_toast.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="toast align-items-center text-white {{ 'bg-success' if success else 'bg-danger' }} border-0" role="alert"
|
||||
aria-live="assertive" aria-atomic="true" style="position: fixed; bottom: 20px; right: 20px; z-index: 1000;">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
{{ message }}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var toastElList = [].slice.call(document.querySelectorAll('.toast'))
|
||||
var toastList = toastElList.map(function (toastEl) {
|
||||
return new bootstrap.Toast(toastEl)
|
||||
})
|
||||
toastList.forEach(toast => toast.show());
|
||||
</script>
|
||||
94
templates/partials/_track_table.html
Normal file
94
templates/partials/_track_table.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Artist</th>
|
||||
<th scope="col">Duration</th>
|
||||
<th scope="col">Spotify</th>
|
||||
<th scope="col">Preview</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Jellyfin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for track in tracks %}
|
||||
<tr>
|
||||
<th scope="row">{{ loop.index }}</th>
|
||||
<td>{{ track.title }}</td>
|
||||
<td>{{ track.artist }}</td>
|
||||
<td>{{ track.duration }}</td>
|
||||
<td>
|
||||
<a href="{{ track.url }}" target="_blank" class="text-success" data-bs-toggle="tooltip" title="Open in Spotify">
|
||||
<i class="fab fa-spotify fa-lg"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if track.preview_url %}
|
||||
<button class="btn btn-sm btn-primary" onclick="playPreview(this, '{{ track.preview_url }}')" data-bs-toggle="tooltip" title="Play Preview">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span data-bs-toggle="tooltip" title="No Preview Available">
|
||||
<button class="btn btn-sm" disabled><i class="fas fa-ban"></i></button>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not track.downloaded %}
|
||||
<button class="btn btn-sm btn-danger"
|
||||
data-bs-toggle="tooltip" title="{{ track.download_status if track.download_status else 'Not downloaded'}}">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-success" data-bs-toggle="tooltip" title="Track downloaded">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if track.jellyfin_id %}
|
||||
<button class="btn btn-sm btn-success" onclick="playJellyfinTrack(this, '{{ track.jellyfin_id }}')" data-bs-toggle="tooltip" title="Play from Jellyfin">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
{% elif track.downloaded %}
|
||||
<span data-bs-toggle="tooltip" title="Track Downloaded, but not in Jellyfin or could not be associated automatically. You can try to do the association manually">
|
||||
{% set title = track.title | replace("'","") %}
|
||||
<button class="btn btn-sm btn-warning" onclick="openSearchModal('{{ title }}','{{track.spotify_id}}')">
|
||||
<i class="fas fa-triangle-exclamation"></i>
|
||||
</button>
|
||||
</span>
|
||||
{% else %}
|
||||
<span data-bs-toggle="tooltip" title="Not Available">
|
||||
<button class="btn btn-sm" disabled><i class="fas fa-ban"></i></button>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="searchModalLabel">Search Jellyfin for Track</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- htmx-enabled form -->
|
||||
<form id="search-form" hx-get="/search_jellyfin" hx-target="#search-results" hx-trigger="submit">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="search-query" name="search_query" placeholder="Search for a track...">
|
||||
<input type="hidden" class="form-control" id="spotify-id" name="spotify_id" >
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="search-results">
|
||||
<!-- Search results will be inserted here by htmx -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
6
templates/partials/_unlinked_tracks_badge.html
Normal file
6
templates/partials/_unlinked_tracks_badge.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if unlinked_track_count > 0 %}
|
||||
<span class="badge rounded-pill bg-danger ms-2 mb-2">
|
||||
{{unlinked_track_count}}
|
||||
<span class="visually-hidden">Unlinked Tracks</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
17
templates/partials/alerts.jinja2
Normal file
17
templates/partials/alerts.jinja2
Normal file
@@ -0,0 +1,17 @@
|
||||
<div id="alerts" hx-swap-oob="true">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="toast align-items-center text-bg-{{ 'success' if category == 'success' else 'danger' }}" role="alert"
|
||||
aria-live="assertive" aria-atomic="true" style="position: fixed; bottom: 20px; right: 20px; z-index: 1000;">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
{{ message }}
|
||||
</div>
|
||||
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
11
templates/search.html
Normal file
11
templates/search.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div id="search-container">
|
||||
{% if query %}
|
||||
<h2>Search Results for "{{ query }}"</h2>
|
||||
{% include 'partials/_searchresults.html' %}
|
||||
{% else %}
|
||||
<p>No search query provided.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
templates/tracks_table.html
Normal file
9
templates/tracks_table.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content%}
|
||||
<div class="container">
|
||||
{% include 'partials/_playlist_info.html' %}
|
||||
|
||||
{% include 'partials/_track_table.html' %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user