Bump version to 0.3.10
Update version number from 0.3.9 to 0.3.10 in Cargo.toml and version.txt files. Also, enhance image loading handling in multiple Vue components (ContainerItem, CurrentTrack, MediaItem, QueueItem, ServerDrawer) by: - Adding ref and watch for tracking image loading states - Implementing proper load and error handlers - Using v-show to control image visibility based on loading state - Adding image state tracking for each item in ServerDrawer - Improving placeholder display logic
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
dependencies = [
|
||||
"axum 0.8.7",
|
||||
"console-subscriber",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "PMOMusic"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { ContainerEntry } from "@/services/pmocontrol/types";
|
||||
import { Folder, Music } from "lucide-vue-next";
|
||||
import ActionMenu from "./ActionMenu.vue";
|
||||
@@ -16,6 +16,19 @@ const emit = defineEmits<{
|
||||
addToQueue: [containerId: string, rendererId: string];
|
||||
}>();
|
||||
|
||||
// Track image loading state
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
// Reset image state when album_art_uri changes
|
||||
watch(
|
||||
() => props.entry.album_art_uri,
|
||||
() => {
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
const cls = props.entry.class.toLowerCase();
|
||||
if (cls.includes("playlist")) return Music;
|
||||
@@ -49,13 +62,13 @@ function handleAddToQueue(rendererId: string) {
|
||||
emit("addToQueue", props.entry.id, rendererId);
|
||||
}
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.style.display = "none";
|
||||
const placeholder = img.nextElementSibling;
|
||||
if (placeholder && placeholder instanceof HTMLElement) {
|
||||
placeholder.style.display = "flex";
|
||||
}
|
||||
function handleImageLoad() {
|
||||
imageLoaded.value = true;
|
||||
imageError.value = false;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
imageError.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -66,18 +79,18 @@ function handleImageError(event: Event) {
|
||||
<!-- Cover avec icône de type en overlay -->
|
||||
<div class="container-cover">
|
||||
<img
|
||||
v-if="entry.album_art_uri"
|
||||
v-if="entry.album_art_uri && !imageError"
|
||||
v-show="imageLoaded"
|
||||
:src="entry.album_art_uri"
|
||||
:alt="entry.title"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div
|
||||
v-if="!entry.album_art_uri || imageError || !imageLoaded"
|
||||
class="cover-placeholder"
|
||||
:style="{
|
||||
display: entry.album_art_uri ? 'none' : 'flex',
|
||||
}"
|
||||
>
|
||||
<component :is="iconComponent" :size="28" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toRef, ref } from "vue";
|
||||
import { computed, toRef, ref, watch } from "vue";
|
||||
import { useRenderer } from "@/composables/useRenderers";
|
||||
import { useUIStore } from "@/stores/ui";
|
||||
import { api } from "@/services/pmocontrol/api";
|
||||
@@ -21,6 +21,10 @@ const seekTargetMs = ref<number | null>(null);
|
||||
const showCoverOverlay = ref(false);
|
||||
const showMetadata = ref(false);
|
||||
|
||||
// Track image loading state
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
function openCoverOverlay() {
|
||||
if (hasCover.value) {
|
||||
showCoverOverlay.value = true;
|
||||
@@ -88,11 +92,26 @@ function formatTime(ms: number | null | undefined): string {
|
||||
const currentTime = computed(() => formatTime(state.value?.position_ms));
|
||||
const totalTime = computed(() => formatTime(state.value?.duration_ms));
|
||||
|
||||
const hasCover = computed(() => !!metadata.value?.album_art_uri);
|
||||
const hasCover = computed(
|
||||
() => !!metadata.value?.album_art_uri && !imageError.value,
|
||||
);
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.style.display = "none";
|
||||
// Reset image state when album_art_uri changes
|
||||
watch(
|
||||
() => metadata.value?.album_art_uri,
|
||||
() => {
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
function handleImageLoad() {
|
||||
imageLoaded.value = true;
|
||||
imageError.value = false;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
imageError.value = true;
|
||||
}
|
||||
|
||||
// Calculer le pourcentage à partir d'une position X dans la barre
|
||||
@@ -339,14 +358,19 @@ const swipeOpacity = computed(() => {
|
||||
@click="openCoverOverlay"
|
||||
>
|
||||
<img
|
||||
v-if="hasCover"
|
||||
:src="metadata?.album_art_uri!"
|
||||
v-if="metadata?.album_art_uri && !imageError"
|
||||
v-show="imageLoaded"
|
||||
:src="metadata.album_art_uri"
|
||||
:alt="metadata?.album || 'Album cover'"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="cover-placeholder">
|
||||
<div
|
||||
v-if="!metadata?.album_art_uri || imageError || !imageLoaded"
|
||||
class="cover-placeholder"
|
||||
>
|
||||
<Music :size="64" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,185 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContainerEntry } from '@/services/pmocontrol/types'
|
||||
import { Music } from 'lucide-vue-next'
|
||||
import ActionMenu from './ActionMenu.vue'
|
||||
import { ref, watch } from "vue";
|
||||
import type { ContainerEntry } from "@/services/pmocontrol/types";
|
||||
import { Music } from "lucide-vue-next";
|
||||
import ActionMenu from "./ActionMenu.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
entry: ContainerEntry
|
||||
serverId: string
|
||||
showActions?: boolean
|
||||
}>()
|
||||
entry: ContainerEntry;
|
||||
serverId: string;
|
||||
showActions?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
playNow: [itemId: string, rendererId: string]
|
||||
addToQueue: [itemId: string, rendererId: string]
|
||||
}>()
|
||||
playNow: [itemId: string, rendererId: string];
|
||||
addToQueue: [itemId: string, rendererId: string];
|
||||
}>();
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
const placeholder = img.nextElementSibling
|
||||
if (placeholder && placeholder instanceof HTMLElement) {
|
||||
placeholder.style.display = 'flex'
|
||||
}
|
||||
// Track image loading state
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
// Reset image state when album_art_uri changes
|
||||
watch(
|
||||
() => props.entry.album_art_uri,
|
||||
() => {
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
function handleImageLoad() {
|
||||
imageLoaded.value = true;
|
||||
imageError.value = false;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
imageError.value = true;
|
||||
}
|
||||
|
||||
function handlePlayNow(rendererId: string) {
|
||||
emit('playNow', props.entry.id, rendererId)
|
||||
emit("playNow", props.entry.id, rendererId);
|
||||
}
|
||||
|
||||
function handleAddToQueue(rendererId: string) {
|
||||
emit('addToQueue', props.entry.id, rendererId)
|
||||
emit("addToQueue", props.entry.id, rendererId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="media-item">
|
||||
<!-- Cover miniature -->
|
||||
<div class="media-cover">
|
||||
<img
|
||||
v-if="entry.album_art_uri"
|
||||
:src="entry.album_art_uri"
|
||||
:alt="entry.album || entry.title"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="cover-placeholder" :style="{ display: entry.album_art_uri ? 'none' : 'flex' }">
|
||||
<Music :size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-item">
|
||||
<!-- Cover miniature -->
|
||||
<div class="media-cover">
|
||||
<img
|
||||
v-if="entry.album_art_uri && !imageError"
|
||||
v-show="imageLoaded"
|
||||
:src="entry.album_art_uri"
|
||||
:alt="entry.album || entry.title"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div
|
||||
v-if="!entry.album_art_uri || imageError || !imageLoaded"
|
||||
class="cover-placeholder"
|
||||
>
|
||||
<Music :size="20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="media-metadata">
|
||||
<div class="media-title">{{ entry.title }}</div>
|
||||
<div class="media-details">
|
||||
<span v-if="entry.artist" class="media-artist">{{ entry.artist }}</span>
|
||||
<span v-if="entry.album" class="media-album">{{ entry.album }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Metadata -->
|
||||
<div class="media-metadata">
|
||||
<div class="media-title">{{ entry.title }}</div>
|
||||
<div class="media-details">
|
||||
<span v-if="entry.artist" class="media-artist">{{
|
||||
entry.artist
|
||||
}}</span>
|
||||
<span v-if="entry.album" class="media-album">{{
|
||||
entry.album
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions menu -->
|
||||
<div class="media-actions">
|
||||
<ActionMenu
|
||||
type="item"
|
||||
:entry-id="entry.id"
|
||||
:server-id="serverId"
|
||||
@play-now="handlePlayNow"
|
||||
@add-to-queue="handleAddToQueue"
|
||||
/>
|
||||
<!-- Actions menu -->
|
||||
<div class="media-actions">
|
||||
<ActionMenu
|
||||
type="item"
|
||||
:entry-id="entry.id"
|
||||
:server-id="serverId"
|
||||
@play-now="handlePlayNow"
|
||||
@add-to-queue="handleAddToQueue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.media-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast);
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.media-item:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-color: var(--color-border);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* Cover */
|
||||
.media-cover {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-tertiary);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* Metadata */
|
||||
.media-metadata {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.media-details {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-artist {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.media-album {
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.media-album::before {
|
||||
content: '•';
|
||||
margin-right: var(--spacing-sm);
|
||||
content: "•";
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.media-actions {
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: var(--color-bg-tertiary);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,139 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import { Music, Play } from 'lucide-vue-next'
|
||||
import type { QueueItem } from '@/services/pmocontrol/types'
|
||||
import { ref, watch } from "vue";
|
||||
import { Music, Play } from "lucide-vue-next";
|
||||
import type { QueueItem } from "@/services/pmocontrol/types";
|
||||
|
||||
defineProps<{
|
||||
item: QueueItem
|
||||
isCurrent: boolean
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
item: QueueItem;
|
||||
isCurrent: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [item: QueueItem]
|
||||
}>()
|
||||
click: [item: QueueItem];
|
||||
}>();
|
||||
|
||||
// Track image loading state
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
// Reset image state when album_art_uri changes
|
||||
watch(
|
||||
() => props.item.album_art_uri,
|
||||
() => {
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
function handleImageLoad() {
|
||||
imageLoaded.value = true;
|
||||
imageError.value = false;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
imageError.value = true;
|
||||
}
|
||||
|
||||
function handleClick(item: QueueItem) {
|
||||
console.log('[QueueItem] Click detected on item:', item.index, item.title)
|
||||
emit('click', item)
|
||||
console.log("[QueueItem] Click detected on item:", item.index, item.title);
|
||||
emit("click", item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['queue-item', { current: isCurrent }]" @click="handleClick(item)">
|
||||
<!-- Indicateur piste en cours -->
|
||||
<div class="current-indicator" v-if="isCurrent">
|
||||
<Play :size="16" fill="currentColor" />
|
||||
</div>
|
||||
<div
|
||||
:class="['queue-item', { current: isCurrent }]"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<!-- Indicateur piste en cours -->
|
||||
<div class="current-indicator" v-if="isCurrent">
|
||||
<Play :size="16" fill="currentColor" />
|
||||
</div>
|
||||
|
||||
<!-- Index (1-based pour l'affichage) -->
|
||||
<span class="item-index">{{ item.index + 1 }}</span>
|
||||
<!-- Index (1-based pour l'affichage) -->
|
||||
<span class="item-index">{{ item.index + 1 }}</span>
|
||||
|
||||
<!-- Cover miniature -->
|
||||
<div class="item-cover">
|
||||
<img
|
||||
v-if="item.album_art_uri"
|
||||
:src="item.album_art_uri"
|
||||
:alt="item.album || 'Album cover'"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@error="(e: Event) => (e.target as HTMLImageElement).style.display = 'none'"
|
||||
/>
|
||||
<Music v-else :size="20" />
|
||||
</div>
|
||||
<!-- Cover miniature -->
|
||||
<div class="item-cover">
|
||||
<img
|
||||
v-if="item.album_art_uri && !imageError"
|
||||
v-show="imageLoaded"
|
||||
:src="item.album_art_uri"
|
||||
:alt="item.album || 'Album cover'"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<Music
|
||||
v-if="!item.album_art_uri || imageError || !imageLoaded"
|
||||
:size="20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Métadonnées -->
|
||||
<div class="item-metadata">
|
||||
<div class="item-title">{{ item.title || 'Sans titre' }}</div>
|
||||
<div class="item-artist">
|
||||
{{ item.artist || 'Artiste inconnu' }}
|
||||
<span v-if="item.album"> • {{ item.album }}</span>
|
||||
</div>
|
||||
<!-- Métadonnées -->
|
||||
<div class="item-metadata">
|
||||
<div class="item-title">{{ item.title || "Sans titre" }}</div>
|
||||
<div class="item-artist">
|
||||
{{ item.artist || "Artiste inconnu" }}
|
||||
<span v-if="item.album"> • {{ item.album }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color var(--transition-fast);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.queue-item:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.queue-item.current {
|
||||
background-color: var(--status-playing-bg);
|
||||
border: 1px solid var(--status-playing);
|
||||
font-weight: 600;
|
||||
background-color: var(--status-playing-bg);
|
||||
border: 1px solid var(--status-playing);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-indicator {
|
||||
position: absolute;
|
||||
left: var(--spacing-xs);
|
||||
color: var(--status-playing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: var(--spacing-xs);
|
||||
color: var(--status-playing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-index {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 2rem;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
min-width: 2rem;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.queue-item.current .item-index {
|
||||
margin-left: var(--spacing-lg);
|
||||
margin-left: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.item-cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.item-metadata {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-item.current .item-title {
|
||||
color: var(--status-playing);
|
||||
color: var(--status-playing);
|
||||
}
|
||||
|
||||
.item-artist {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed, watch, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
X,
|
||||
@@ -53,6 +53,29 @@ const isLoading = ref(false);
|
||||
// État du menu dropdown (pour chaque item, on stocke si son menu est ouvert)
|
||||
const openMenuId = ref<string | null>(null);
|
||||
|
||||
// Track image loading state per item
|
||||
const imageStates = reactive<Map<string, { loaded: boolean; error: boolean }>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
function getImageState(itemId: string) {
|
||||
if (!imageStates.has(itemId)) {
|
||||
imageStates.set(itemId, { loaded: false, error: false });
|
||||
}
|
||||
return imageStates.get(itemId)!;
|
||||
}
|
||||
|
||||
function handleImageLoad(itemId: string) {
|
||||
const state = getImageState(itemId);
|
||||
state.loaded = true;
|
||||
state.error = false;
|
||||
}
|
||||
|
||||
function handleImageError(itemId: string) {
|
||||
const state = getImageState(itemId);
|
||||
state.error = true;
|
||||
}
|
||||
|
||||
// Rafraîchir la liste quand le drawer s'ouvre
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -65,6 +88,7 @@ watch(
|
||||
browseData.value = null;
|
||||
clearPath();
|
||||
closeMenu();
|
||||
imageStates.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -470,33 +494,25 @@ function handleSettingsClick() {
|
||||
<!-- Cover avec image ou icône -->
|
||||
<div class="content-cover">
|
||||
<img
|
||||
v-if="item.album_art_uri"
|
||||
v-if="
|
||||
item.album_art_uri &&
|
||||
!getImageState(item.id).error
|
||||
"
|
||||
v-show="getImageState(item.id).loaded"
|
||||
:src="item.album_art_uri"
|
||||
:alt="item.title"
|
||||
class="cover-image"
|
||||
loading="lazy"
|
||||
@error="
|
||||
(e) => {
|
||||
(
|
||||
e.target as HTMLImageElement
|
||||
).style.display = 'none';
|
||||
const placeholder = (
|
||||
e.target as HTMLElement
|
||||
)
|
||||
.nextElementSibling as HTMLElement;
|
||||
if (placeholder)
|
||||
placeholder.style.display =
|
||||
'flex';
|
||||
}
|
||||
"
|
||||
@load="handleImageLoad(item.id)"
|
||||
@error="handleImageError(item.id)"
|
||||
/>
|
||||
<div
|
||||
v-if="
|
||||
!item.album_art_uri ||
|
||||
getImageState(item.id).error ||
|
||||
!getImageState(item.id).loaded
|
||||
"
|
||||
class="cover-placeholder"
|
||||
:style="{
|
||||
display: item.album_art_uri
|
||||
? 'none'
|
||||
: 'flex',
|
||||
}"
|
||||
>
|
||||
<Folder
|
||||
v-if="item.is_container"
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.3.9
|
||||
0.3.10
|
||||
|
||||
Reference in New Issue
Block a user