Merge pull request 'Bump version to 0.3.10' (#64) from push-koulrlvkzszm into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 28m19s
All checks were successful
Build and Push Docker Image / build (push) Successful in 28m19s
Reviewed-on: #64
This commit was merged in pull request #64.
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