Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { libraries } from "$lib/stores/library";
|
||||
import { toast } from "$lib/stores/toast";
|
||||
import CreatePlaylistModal from "./CreatePlaylistModal.svelte";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
trackIds: string[];
|
||||
}
|
||||
|
||||
let { isOpen = false, onClose, trackIds }: Props = $props();
|
||||
|
||||
let playlists = $state<MediaItem[]>([]);
|
||||
let loading = $state(true);
|
||||
let adding = $state<string | null>(null);
|
||||
let showCreateModal = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) loadPlaylists();
|
||||
});
|
||||
|
||||
async function loadPlaylists() {
|
||||
loading = true;
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
// Find music library for playlist browsing
|
||||
const musicLib = $libraries.find(lib => lib.collectionType === "music");
|
||||
if (musicLib) {
|
||||
const result = await repo.getItems(musicLib.id, { includeItemTypes: ["Playlist"], limit: 100 });
|
||||
playlists = result.items;
|
||||
} else {
|
||||
// Try searching for playlists without a parent
|
||||
const result = await repo.search("", { includeItemTypes: ["Playlist"], limit: 100 });
|
||||
playlists = result.items;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[AddToPlaylist] Failed to load playlists:", e);
|
||||
toast.error("Failed to load playlists");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddToPlaylist(playlist: MediaItem) {
|
||||
adding = playlist.id;
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
await repo.addToPlaylist(playlist.id, trackIds);
|
||||
toast.success(`Added to "${playlist.name}"`);
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
console.error("[AddToPlaylist] Failed to add:", e);
|
||||
toast.error("Failed to add to playlist");
|
||||
} finally {
|
||||
adding = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleNewPlaylist() {
|
||||
showCreateModal = true;
|
||||
}
|
||||
|
||||
function handleCreateModalClose() {
|
||||
showCreateModal = false;
|
||||
onClose?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||
onclick={onClose}
|
||||
onkeydown={(e) => { if (e.key === "Escape") onClose?.(); }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="bg-[var(--color-surface)] rounded-xl p-6 max-w-sm w-full mx-4 space-y-4 max-h-[70vh] flex flex-col"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-white">Add to Playlist</h3>
|
||||
|
||||
<!-- New Playlist button -->
|
||||
<button
|
||||
onclick={handleNewPlaylist}
|
||||
class="w-full flex items-center gap-3 p-3 bg-[var(--color-background)] hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors"
|
||||
>
|
||||
<div class="w-10 h-10 bg-[var(--color-jellyfin)] rounded flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-white font-medium">New Playlist</span>
|
||||
</button>
|
||||
|
||||
<!-- Playlist list -->
|
||||
<div class="overflow-y-auto flex-1 space-y-1">
|
||||
{#if loading}
|
||||
{#each Array(3) as _}
|
||||
<div class="animate-pulse flex items-center gap-3 p-3">
|
||||
<div class="w-10 h-10 bg-gray-700 rounded"></div>
|
||||
<div class="h-4 bg-gray-700 rounded w-1/2"></div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if playlists.length === 0}
|
||||
<p class="text-gray-400 text-center py-4">No playlists yet</p>
|
||||
{:else}
|
||||
{#each playlists as playlist (playlist.id)}
|
||||
<button
|
||||
onclick={() => handleAddToPlaylist(playlist)}
|
||||
disabled={adding === playlist.id}
|
||||
class="w-full flex items-center gap-3 p-3 hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div class="w-10 h-10 flex-shrink-0 rounded overflow-hidden">
|
||||
{#if playlist.primaryImageTag}
|
||||
<CachedImage
|
||||
itemId={playlist.id}
|
||||
imageType="Primary"
|
||||
tag={playlist.primaryImageTag}
|
||||
maxWidth={80}
|
||||
alt={playlist.name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full h-full bg-gray-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-white text-left truncate">
|
||||
{adding === playlist.id ? "Adding..." : playlist.name}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="w-full px-4 py-2 bg-[var(--color-surface-hover)] hover:bg-gray-600 rounded-lg transition-colors text-center"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CreatePlaylistModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={handleCreateModalClose}
|
||||
initialItemIds={trackIds}
|
||||
/>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { toast } from "$lib/stores/toast";
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
initialItemIds?: string[];
|
||||
}
|
||||
|
||||
let { isOpen = false, onClose, initialItemIds = [] }: Props = $props();
|
||||
|
||||
let name = $state("");
|
||||
let creating = $state(false);
|
||||
|
||||
async function handleCreate() {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
creating = true;
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
const result = await repo.createPlaylist(trimmed, initialItemIds.length > 0 ? initialItemIds : undefined);
|
||||
toast.success(`Playlist "${trimmed}" created`);
|
||||
name = "";
|
||||
onClose?.();
|
||||
goto(`/library/${result.id}`);
|
||||
} catch (e) {
|
||||
console.error("[CreatePlaylist] Failed:", e);
|
||||
toast.error("Failed to create playlist");
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && name.trim()) handleCreate();
|
||||
if (e.key === "Escape") onClose?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||
onclick={onClose}
|
||||
onkeydown={(e) => { if (e.key === "Escape") onClose?.(); }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="bg-[var(--color-surface)] rounded-xl p-6 max-w-sm w-full mx-4 space-y-4"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-white">Create Playlist</h3>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={name}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Playlist name"
|
||||
class="w-full px-4 py-2 bg-[var(--color-background)] border border-gray-600 rounded-lg text-white placeholder-gray-500 outline-none focus:border-[var(--color-jellyfin)] transition-colors"
|
||||
/>
|
||||
|
||||
{#if initialItemIds.length > 0}
|
||||
<p class="text-sm text-gray-400">
|
||||
{initialItemIds.length} track{initialItemIds.length !== 1 ? "s" : ""} will be added.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="px-4 py-2 bg-[var(--color-surface-hover)] hover:bg-gray-600 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={handleCreate}
|
||||
disabled={!name.trim() || creating}
|
||||
class="px-4 py-2 bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin-dark)] disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors"
|
||||
>
|
||||
{creating ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user