Skip to content
Open
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
2 changes: 2 additions & 0 deletions src-tauri/src/plugins/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub struct Config {
pub auto_update_checker: bool,
pub notifications: bool,
pub track_view_density: TrackViewDensity,
pub track_view_covers: bool,
pub wayland_compat: bool,
#[serde(default)]
pub menu_bar_visible: bool,
Expand Down Expand Up @@ -108,6 +109,7 @@ impl Default for Config {
auto_update_checker: true,
notifications: false,
track_view_density: TrackViewDensity::Normal,
track_view_covers: false,
wayland_compat: false,
menu_bar_visible: false,
}
Expand Down
5 changes: 5 additions & 0 deletions src/api/SettingsAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ async function setTracksDensity(
await ConfigBridge.set('track_view_density', density);
}

async function toggleTracksCovers(value: boolean): Promise<void> {
await ConfigBridge.set('track_view_covers', value);
}

const setUIMainColor = async (
mainColor: Config['ui_accent_color'],
): Promise<void> => {
Expand Down Expand Up @@ -228,6 +232,7 @@ const SettingsAPI = {
setUIMainColor,
applyUIMainColorToUI,
setTracksDensity,
toggleTracksCovers,
checkForUpdate,
toggleLibraryAutorefresh,
toggleAutoUpdateChecker,
Expand Down
5 changes: 3 additions & 2 deletions src/components/Cover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Icon, { type IconSize } from './Icon';
type Props = {
track: Track;
noHorizontalBorder?: boolean;
decorative?: boolean;
iconSize?: IconSize;
};

Expand All @@ -31,9 +32,9 @@ export default function Cover(props: Props) {
</div>
<img
src={coverPath ?? undefined}
alt={t`Album cover`}
alt={props.decorative === true ? '' : t`Album cover`}
draggable={false}
aria-hidden={coverPath === null}
aria-hidden={props.decorative === true || coverPath === null}
{...stylex.props(styles.image, coverPath != null && styles.visible)}
/>
</div>
Expand Down
43 changes: 35 additions & 8 deletions src/components/PlayingIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,46 @@ import { usePlayerState } from '../hooks/usePlayer';
import player from '../lib/player';
import Icon from './Icon';

export default function TrackPlayingIndicator() {
type Props = {
size?: 'default' | 'large';
};

export default function TrackPlayingIndicator({ size = 'default' }: Props) {
const [hovered, setHovered] = useState(false);
const isPaused = usePlayerState((state) => state.isPaused);
const isLarge = size === 'large';

const icon = useMemo(() => {
if (!isPaused) {
if (hovered) {
return <Icon name="pause" size={12} />;
return <Icon name="pause" size={isLarge ? 16 : 12} />;
}

return (
<div {...stylex.props(styles.animation)}>
<div {...stylex.props(styles.bar)} />
<div {...stylex.props(styles.bar, styles.barSecond)} />
<div {...stylex.props(styles.bar, styles.barThird)} />
<div
{...stylex.props(styles.animation, isLarge && styles.animationLarge)}
>
<div {...stylex.props(styles.bar, isLarge && styles.barLarge)} />
<div
{...stylex.props(
styles.bar,
isLarge && styles.barLarge,
styles.barSecond,
)}
/>
<div
{...stylex.props(
styles.bar,
isLarge && styles.barLarge,
styles.barThird,
)}
/>
</div>
);
}

return <Icon name="play" />;
}, [isPaused, hovered]);
return <Icon name="play" size={isLarge ? 16 : undefined} />;
}, [isPaused, hovered, isLarge]);

return (
<button
Expand Down Expand Up @@ -67,6 +86,10 @@ const styles = stylex.create({
alignItems: 'flex-end',
justifyContent: 'space-between',
},
animationLarge: {
width: '16px',
height: '18px',
},
bar: {
height: '8px',
width: '2px',
Expand All @@ -78,6 +101,10 @@ const styles = stylex.create({
animationIterationCount: 'infinite',
transformOrigin: 'bottom',
},
barLarge: {
height: '18px',
width: '4px',
},
barSecond: {
animationDelay: '0.55s',
},
Expand Down
18 changes: 16 additions & 2 deletions src/components/TrackList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import TrackListGrouped from './TrackListGrouped';
type TrackListProps = {
playlists: Playlist[];
tracksDensity: Config['track_view_density'];
showCoverThumbnails: boolean;
reorderable?: boolean;
onReorder?: (tracks: Track[]) => void;
queueOrigin: QueueOrigin;
Expand All @@ -66,12 +67,15 @@ type Props = TrackListDefaultLayoutProps | TrackListGroupedLayoutProps;

const ROW_HEIGHT = 30;
const ROW_HEIGHT_COMPACT = 24;
const ROW_HEIGHT_WITH_COVERS = 52;
const ROW_HEIGHT_COMPACT_WITH_COVERS = 44;

export default function TrackList(props: Props) {
const {
layout,
data,
tracksDensity,
showCoverThumbnails,
reorderable,
queueOrigin,
onReorder,
Expand Down Expand Up @@ -420,8 +424,17 @@ export default function TrackList(props: Props) {
],
);

const rowHeight =
tracksDensity === 'compact' ? ROW_HEIGHT_COMPACT : ROW_HEIGHT;
// Grouped lists are not virtualized yet, so thumbnail rows stay limited to
// the default layout to avoid loading covers for every track at once.
const showCoverThumbnailsInRows = layout === 'default' && showCoverThumbnails;

const rowHeight = showCoverThumbnailsInRows
? tracksDensity === 'compact'
? ROW_HEIGHT_COMPACT_WITH_COVERS
: ROW_HEIGHT_WITH_COVERS
: tracksDensity === 'compact'
? ROW_HEIGHT_COMPACT
: ROW_HEIGHT;

return (
<div
Expand All @@ -439,6 +452,7 @@ export default function TrackList(props: Props) {
ref={scrollableRef}
tracks={data}
rowHeight={rowHeight}
showCoverThumbnails={showCoverThumbnailsInRows}
selectedTracks={selectedTracks}
reorderable={reorderable}
initialOffset={getScrollPosition()}
Expand Down
8 changes: 7 additions & 1 deletion src/components/TrackListDefault.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Props = {
selectedTracks: Set<string>;
initialOffset: number;
rowHeight: number;
showCoverThumbnails: boolean;
onReorder?: (tracks: Track[]) => void;
} & TrackRowEvents;

Expand All @@ -44,6 +45,7 @@ export default function TrackListDefault(props: Props) {
selectedTracks,
initialOffset,
rowHeight,
showCoverThumbnails,
onReorder,
onTrackSelect,
onContextMenu,
Expand Down Expand Up @@ -115,7 +117,10 @@ export default function TrackListDefault(props: Props) {
sensors={sensors}
>
<Scrollable ref={innerScrollableRef}>
<TrackListHeader sortable={!reorderable} />
<TrackListHeader
sortable={!reorderable}
showCoverThumbnails={showCoverThumbnails}
/>

{/* The large inner element to hold all of the items */}
<ul
Expand All @@ -140,6 +145,7 @@ export default function TrackListDefault(props: Props) {
selected={selectedTracks.has(track.id)}
track={track}
isPlaying={trackPlayingID === track.id}
showCoverThumbnail={showCoverThumbnails}
index={virtualItem.index}
onTrackSelect={onTrackSelect}
onContextMenu={onContextMenu}
Expand Down
15 changes: 13 additions & 2 deletions src/components/TrackListHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import TrackListHeaderCell from './TrackListHeaderCell';

type Props = {
sortable?: boolean;
showCoverThumbnails: boolean;
};

export default function TrackListHeader({ sortable = true }: Props) {
export default function TrackListHeader({
sortable = true,
showCoverThumbnails,
}: Props) {
const { t } = useLingui();

return (
Expand All @@ -17,7 +21,11 @@ export default function TrackListHeader({ sortable = true }: Props) {
aria-label={t`Track list sorting options`}
>
<TrackListHeaderCell
xstyle={styles.cellTrackPlaying}
xstyle={
showCoverThumbnails
? styles.cellTrackPlayingWithCover
: styles.cellTrackPlaying
}
title="&nbsp;"
sortBy={null}
/>
Expand Down Expand Up @@ -66,6 +74,9 @@ const styles = stylex.create({
cellTrackPlaying: {
width: '30px',
},
cellTrackPlayingWithCover: {
width: '56px',
},
cellTrack: {
flexGrow: 1,
flexShrink: 1,
Expand Down
38 changes: 36 additions & 2 deletions src/components/TrackRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type React from 'react';

import type { Track } from '../generated/typings';
import useFormattedDuration from '../hooks/useFormattedDuration';
import Cover from './Cover';
import PlayingIndicator from './PlayingIndicator';

export type TrackRowEvents = {
Expand All @@ -28,6 +29,7 @@ type Props = {
isPlaying?: boolean;
draggable?: boolean;
simplified?: boolean;
showCoverThumbnail?: boolean;
showArtistInTitle?: boolean;
style?: React.CSSProperties;
} & TrackRowEvents;
Expand Down Expand Up @@ -100,8 +102,24 @@ export default function TrackRow(props: Props) {
)}
style={props.style}
>
<div {...stylex.props(cellStyles.cell, cellStyles.trackPlaying)}>
{props.isPlaying ? <PlayingIndicator /> : null}
<div
{...stylex.props(
cellStyles.cell,
cellStyles.trackPlaying,
props.showCoverThumbnail === true && cellStyles.trackCoverSlot,
)}
>
{props.showCoverThumbnail === true ? (
<div {...stylex.props(cellStyles.coverThumbnail)}>
{props.isPlaying ? (
<PlayingIndicator size="large" />
) : (
<Cover track={track} decorative iconSize={12} />
)}
</div>
) : props.isPlaying ? (
<PlayingIndicator />
) : null}
</div>
<div {...stylex.props(cellStyles.cell, cellStyles.title)}>
<span {...stylex.props(cellStyles.titleText)}>{track.title}</span>
Expand Down Expand Up @@ -206,6 +224,22 @@ const cellStyles = stylex.create({
width: '30px',
flexShrink: 0,
},
trackCoverSlot: {
width: '56px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: '8px',
paddingRight: '8px',
lineHeight: 1,
},
coverThumbnail: {
width: '38px',
aspectRatio: '1',
borderRadius: '4px',
overflow: 'hidden',
backgroundColor: 'var(--cover-bg)',
},
title: {
flexGrow: 1,
flexShrink: 1,
Expand Down
2 changes: 1 addition & 1 deletion src/generated/typings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type Config = { language: string, theme: string, ui_accent_color: string | null, audio_volume: number, audio_playback_rate: number | null, audio_follow_playing_track: boolean, audio_muted: boolean, audio_shuffle: boolean, audio_repeat: Repeat, audio_stream_server: boolean, default_view: DefaultView, library_sort_by: SortBy, library_sort_order: SortOrder, library_folders: Array<string>, library_autorefresh: boolean, sleepblocker: boolean, auto_update_checker: boolean, notifications: boolean, track_view_density: TrackViewDensity, wayland_compat: boolean, menu_bar_visible: boolean, };
export type Config = { language: string, theme: string, ui_accent_color: string | null, audio_volume: number, audio_playback_rate: number | null, audio_follow_playing_track: boolean, audio_muted: boolean, audio_shuffle: boolean, audio_repeat: Repeat, audio_stream_server: boolean, default_view: DefaultView, library_sort_by: SortBy, library_sort_order: SortOrder, library_folders: Array<string>, library_autorefresh: boolean, sleepblocker: boolean, auto_update_checker: boolean, notifications: boolean, track_view_density: TrackViewDensity, track_view_covers: boolean, wayland_compat: boolean, menu_bar_visible: boolean, };

export type DefaultView = "Library" | "Artists" | "Playlists";

Expand Down
1 change: 1 addition & 0 deletions src/lib/__mocks__/bridge-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const MOCK_CONFIG: Config = {
auto_update_checker: true,
notifications: false,
track_view_density: 'normal',
track_view_covers: false,
wayland_compat: false,
menu_bar_visible: false,
};
Expand Down
1 change: 1 addition & 0 deletions src/routes/artists.$artistID.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default function ViewArtistDetails() {
layout="grouped"
data={content}
tracksDensity={config.track_view_density}
showCoverThumbnails={config.track_view_covers}
playlists={playlists}
queueOrigin={queueOrigin}
/>
Expand Down
1 change: 1 addition & 0 deletions src/routes/artists.presets.compilations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function ViewCompilations() {
layout="grouped"
data={content}
tracksDensity={config.track_view_density}
showCoverThumbnails={config.track_view_covers}
playlists={playlists}
queueOrigin={queueOrigin}
showArtistInTitle={true}
Expand Down
1 change: 1 addition & 0 deletions src/routes/library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function ViewLibrary() {
data={filteredTracks}
queueOrigin={QUEUE_ORIGIN}
tracksDensity={config.track_view_density}
showCoverThumbnails={config.track_view_covers}
playlists={playlists}
/>
</TrackListStates>
Expand Down
1 change: 1 addition & 0 deletions src/routes/playlists.$playlistID.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ function ViewPlaylistDetails() {
layout="default"
data={filteredTracks}
tracksDensity={config.track_view_density}
showCoverThumbnails={config.track_view_covers}
playlists={playlists}
queueOrigin={queueOrigin}
onReorder={onReorder}
Expand Down
8 changes: 8 additions & 0 deletions src/routes/settings.ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ function ViewSettingsUI() {
</option>
</Setting.Select>
</Setting.Section>
<Setting.Section>
<CheckboxSetting
title={t`Show track covers`}
description={t`Display album cover thumbnails in track rows`}
value={config.track_view_covers}
onChange={useInvalidateCallback(SettingsAPI.toggleTracksCovers)}
/>
</Setting.Section>
<Setting.Section>
<Setting.Select
label={t`Default view`}
Expand Down
Loading