Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/components/ItemsListing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,17 @@
v-if="viewMode == 'list'"
:item-height="70"
height="100%"
:items="pagedItems"
:items="listDisplayItems"
style="height: 100%; overflow: hidden"
>
<template #default="{ item }">
<div v-if="'isDiscHeader' in item" class="disc-header">
{{ $t("disc", { number: item.disc }) }}
</div>
<ListviewItem
v-else
:item="item"
:album-track-view="itemtype === 'albumtracks'"
:show-track-number="showTrackNumber"
:show-disc-number="showTrackNumber"
:show-duration="showDuration"
Expand Down Expand Up @@ -384,6 +389,36 @@ const initialDataReceived = ref(false);
const tempHide = ref(false);
const genreOptions = ref<{ label: string; value: number }[]>([]);

interface DiscHeader {
isDiscHeader: true;
disc: number;
}

const discNumber = (i: MediaItemType) =>
"disc_number" in i ? i.disc_number : undefined;

// for multi-disc albums (in default track order), insert a "Disc X" header
// before the first track of each disc.
const listDisplayItems = computed<(MediaItemType | DiscHeader)[]>(() => {
const multiDisc =
props.itemtype === "albumtracks" &&
allItems.value.some((i) => (discNumber(i) ?? 0) > 1);
if (!multiDisc || params.value.sortBy !== "track_number") {
return pagedItems.value;
}
const result: (MediaItemType | DiscHeader)[] = [];
let lastDisc: number | undefined;
for (const item of pagedItems.value) {
const disc = discNumber(item);
if (disc && disc !== lastDisc) {
result.push({ isDiscHeader: true, disc });
lastDisc = disc;
}
result.push(item);
}
return result;
});

// methods
const applyQueryGenreFilter = function () {
const queryGenre = route.query.genre_id ?? route.query.genre_ids;
Expand Down Expand Up @@ -1710,6 +1745,16 @@ defineExpose({
</script>

<style scoped>
.disc-header {
display: flex;
align-items: flex-end;
height: 100%;
padding: 16px 8px 8px;
font-size: 1.15rem;
font-weight: 600;
opacity: 0.7;
}

/* ThumbView panel columns */
.col-2 {
width: 50%;
Expand Down
107 changes: 103 additions & 4 deletions src/components/ListviewItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,34 @@
/>
</label>
</div>
<div v-else class="media-thumb listitem-media-thumb">
<MediaItemThumb size="50" :item="isAvailable ? item : undefined" />
<div v-else class="listitem-prepend">
<div
v-if="
albumTrackView &&
showTrackNumber &&
'track_number' in item &&
item.track_number
"
class="track-number"
>
{{ item.track_number }}
</div>
<div
v-if="albumTrackView && item.is_playable"
class="listitem-play-thumb"
>
<v-btn
icon
variant="text"
:disabled="disablePlayButton"
@click.stop="onPlayClick"
>
<v-icon size="24">mdi-play-circle-outline</v-icon>
</v-btn>
</div>
<div v-else class="media-thumb listitem-media-thumb">
<MediaItemThumb size="50" :item="isAvailable ? item : undefined" />
</div>
</div>
</template>

Expand All @@ -40,14 +66,23 @@
:item="item"
:show-checkboxes="showCheckboxes"
:is-playing="isPlaying"
:album-track-view="albumTrackView"
/>
</template>

<!-- subtitle -->
<template #subtitle>
<!-- album track view: only show collaborating artist(s), if any -->
<template v-if="albumTrackView">
<div v-if="collabArtists" class="ma-line-clamp-1">
{{ $t("with_artists", { artists: collabArtists }) }}
</div>
</template>
<!-- track: artists(s) + album (check for provider_mappings to filter out ItemMapping) -->
<div
v-if="item.media_type == MediaType.TRACK && 'provider_mappings' in item"
v-else-if="
item.media_type == MediaType.TRACK && 'provider_mappings' in item
"
class="ma-line-clamp-1"
>
<v-item-group>
Expand Down Expand Up @@ -148,6 +183,20 @@
</v-tooltip>
</v-img>

<!-- track duration (album track view) -->
<span
v-if="
albumTrackView &&
showDuration &&
'duration' in item &&
item.duration &&
!$vuetify.display.mobile
"
Comment thread
stvncode marked this conversation as resolved.
class="track-duration"
>
{{ formatDuration(item.duration) }}
</span>

<!-- provider icon -->
<provider-icon
v-if="getBreakpointValue('bp2') && showProvider"
Expand Down Expand Up @@ -186,7 +235,11 @@

<!-- play button -->
<v-btn
v-if="item.is_playable && (showPlayButton ?? getBreakpointValue('bp0'))"
v-if="
!albumTrackView &&
item.is_playable &&
(showPlayButton ?? getBreakpointValue('bp0'))
"
icon
variant="text"
size="small"
Expand Down Expand Up @@ -232,6 +285,7 @@
// properties
export interface Props {
item: MediaItemType;
albumTrackView?: boolean;
showTrackNumber?: boolean;
showDiscNumber?: boolean;
showPosition?: boolean;
Expand All @@ -249,7 +303,7 @@
showPlayButton?: boolean;
disablePlayButton?: boolean;
parentItem?: MediaItemType;
sortBy?: string;

Check warning on line 306 in src/components/ListviewItem.vue

View workflow job for this annotation

GitHub Actions / Test and Lint

Prop 'sortBy' requires default value to be set

Check warning on line 306 in src/components/ListviewItem.vue

View workflow job for this annotation

GitHub Actions / Test and Lint

Prop 'sortBy' requires default value to be set
}

// global refs
Expand All @@ -268,6 +322,7 @@
});

const compProps = withDefaults(defineProps<Props>(), {
albumTrackView: false,
showTrackNumber: true,
showDiscNumber: true,
showProvider: true,
Expand All @@ -285,6 +340,19 @@
});

// computed properties
const collabArtists = computed(() => {
if (!("artists" in compProps.item) || !compProps.item.artists) return "";
const albumArtists =
compProps.parentItem && "artists" in compProps.parentItem
? compProps.parentItem.artists
: [];
const albumNames = new Set(albumArtists.map((a) => a.name.toLowerCase()));
const collab = compProps.item.artists.filter(
(a) => !albumNames.has(a.name.toLowerCase()),
);
return collab.map((a) => a.name).join(" | ");
});

const HiResDetails = computed(() => {
if (!("provider_mappings" in compProps.item)) return "";
for (const prov of compProps.item.provider_mappings) {
Expand Down Expand Up @@ -365,6 +433,37 @@
opacity: 0.3;
}

.listitem-prepend {
display: flex;
align-items: center;
}

.listitem-play-thumb {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}

.track-number {
display: flex;
align-items: center;
justify-content: center;
min-width: 22px;
margin-right: 2px;
font-size: 0.875rem;
opacity: 0.7;
font-variant-numeric: tabular-nums;
}

.track-duration {
font-size: 0.875rem;
opacity: 0.7;
margin-right: 12px;
white-space: nowrap;
}

.dimmed {
opacity: 0.3;
}
Expand Down
32 changes: 30 additions & 2 deletions src/components/ListviewItemTitle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
</span>
<span
v-else
:class="{ 'is-playing': isPlaying, 'checkbox-label': showCheckboxes }"
:class="{
'is-playing': isPlaying,
'checkbox-label': showCheckboxes,
'album-track-title': albumTrackView,
}"
class="v-list-item-title"
>
{{ displayName }}
Expand All @@ -25,7 +29,11 @@
>
<Tooltip>
<TooltipTrigger as-child>
<span :aria-label="t('tooltip.explicit')" tabindex="0">
<span
:aria-label="t('tooltip.explicit')"
tabindex="0"
:class="{ 'explicit-icon-align': albumTrackView }"
>
<v-icon
:class="{ 'explicit-icon-margin-left': showCheckboxes }"
icon="mdi-alpha-e-box"
Expand Down Expand Up @@ -61,6 +69,7 @@ export interface Props {
item: MediaItemType;
showCheckboxes: boolean;
isPlaying: boolean;
albumTrackView?: boolean;
}

// global refs
Expand All @@ -70,6 +79,7 @@ withDefaults(defineProps<Props>(), {
displayName: "",
showCheckboxes: false,
isPlaying: false,
albumTrackView: false,
});
</script>

Expand All @@ -78,6 +88,24 @@ withDefaults(defineProps<Props>(), {
margin-left: 23px;
}

.album-track-title {
font-size: 0.9375rem;
font-weight: 500;
white-space: normal;
overflow: hidden;
display: -webkit-inline-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
vertical-align: middle;
}

/* Vertically center the explicit icon against the title in the album track view. */
.explicit-icon-align {
display: inline-flex;
vertical-align: middle;
margin-left: 12px;
}

/* When checkbox is displayed, explicit icon will be shown to the right of the title.
This adds a bit of spacing between the title and the explicit icon. */
.explicit-icon-margin-left {
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@
"try_global_search": "Try global search",
"type_to_search": "Type here to search...",
"volume_normalization": "Volume normalization",
"with_artists": "with {artists}",
Comment thread
stvncode marked this conversation as resolved.
"play_now_replace": "Play Now (clear queue)",
"currently_playing": "Currently playing",
"favorites_add": "Add to favorites",
Expand Down Expand Up @@ -1245,6 +1246,7 @@
"series_singular": "Series",
"queue_move_end": "Move to end",
"discover": "Discover",
"disc": "Disc {number}",
Comment thread
stvncode marked this conversation as resolved.
"save_queue_as_playlist": "Save queue as playlist",
"playlist_created": "Playlist created",
"open_playlist": "Open playlist",
Expand Down
Loading