Skip to content

Commit

Permalink
wip details
Browse files Browse the repository at this point in the history
  • Loading branch information
martpie committed Jan 2, 2025
1 parent cebeaf1 commit 34d4941
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 18 deletions.
1 change: 1 addition & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fn main() {
"get_tracks",
"update_track",
"get_artists",
"get_artist_tracks",
"get_all_playlists",
"get_playlist",
"create_playlist",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/capabilities/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"database:allow-update-track",
"database:allow-remove-tracks",
"database:allow-get-artists",
"database:allow-get-artist-tracks",
"database:allow-get-all-playlists",
"database:allow-get-playlist",
"database:allow-create-playlist",
Expand Down
47 changes: 40 additions & 7 deletions src-tauri/src/libs/database.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use log::info;
use ormlite::model::ModelBuilder;
use ormlite::sqlite::SqliteConnection;
use ormlite::{Model, TableMeta};
Expand All @@ -7,7 +6,7 @@ use std::path::PathBuf;

use super::error::AnyResult;
use super::playlist::Playlist;
use super::track::Track;
use super::track::{Track, TrackGroup};
use super::utils::TimeLogger;

// KEEP THAT IN SYNC with Tauri's file associations in tauri.conf.json
Expand Down Expand Up @@ -147,18 +146,17 @@ impl DB {
}

/**
* Insert a new track in the DB, will fail in case there is a duplicate unique
* key (like track.path)
*
* Doc: https://github.com/khonsulabs/bonsaidb/blob/main/examples/basic-local/examples/basic-local-multidb.rs
* Get the list of artist registered in the database.
* Only fetches the first artist for each row.
*/
pub async fn get_artists(&mut self) -> AnyResult<Vec<String>> {
let timer = TimeLogger::new("Retrieved artists".into());

let query = format!(
"SELECT DISTINCT JSON_EXTRACT({}, '$[0]') FROM {};",
"artists",
Track::table_name()
);
info!("query for all artists: {}", query);

let mut result: Vec<String> = ormlite::query_as(&query)
.fetch_all(&mut self.connection)
Expand All @@ -170,9 +168,44 @@ impl DB {
// sort them alphabetically
result.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));

timer.complete();
Ok(result)
}

/**
* Get the list of artist registered in the database.
* Only fetches the first artist for each row.
*/
pub async fn get_artist_tracks(&mut self, artist: String) -> AnyResult<Vec<TrackGroup>> {
let timer = TimeLogger::new("Retrieved tracks for artist".into());

let tracks = Track::select()
.where_bind("JSON_EXTRACT(artists, '$[0]') = ?", &artist)
.fetch_all(&mut self.connection)
.await?;

// Manual group_by because ormlite is weird in this regard
let mut groups: HashMap<String, Vec<Track>> = HashMap::new();

for item in tracks {
groups
.entry(item.album.clone())
.or_insert_with(Vec::new)
.push(item);
}

let track_groups = groups
.into_iter()
.map(|(album, tracks)| TrackGroup {
label: album,
tracks,
})
.collect();

timer.complete();
Ok(track_groups)
}

/** Get all the playlists (and their content) from the database */
pub async fn get_all_playlists(&mut self) -> AnyResult<Vec<Playlist>> {
let timer = TimeLogger::new("Retrieved and decoded playlists".into());
Expand Down
12 changes: 12 additions & 0 deletions src-tauri/src/libs/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ pub struct Track {
pub disk_of: Option<u32>,
}

/**
* Represents a group of tracks, grouped by "something", lib artist name, or
* album name
*/
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../src/generated/typings/index.ts")]

pub struct TrackGroup {
pub label: String,
pub tracks: Vec<Track>,
}

/**
* Generate a Track struct from a Path, or nothing if it is not a valid audio
* file
Expand Down
11 changes: 10 additions & 1 deletion src-tauri/src/plugins/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::libs::database::{DB, SUPPORTED_PLAYLISTS_EXTENSIONS, SUPPORTED_TRACKS
use crate::libs::error::{AnyResult, MuseeksError};
use crate::libs::events::IPCEvent;
use crate::libs::playlist::Playlist;
use crate::libs::track::{get_track_from_file, get_track_id_for_path, Track};
use crate::libs::track::{get_track_from_file, get_track_id_for_path, Track, TrackGroup};
use crate::libs::utils::{scan_dirs, TimeLogger};

use super::config::get_storage_dir;
Expand Down Expand Up @@ -295,6 +295,14 @@ async fn get_artists(db_state: State<'_, DBState>) -> AnyResult<Vec<String>> {
db_state.get_lock().await.get_artists().await
}

#[tauri::command]
async fn get_artist_tracks(
db_state: State<'_, DBState>,
artist: String,
) -> AnyResult<Vec<TrackGroup>> {
db_state.get_lock().await.get_artist_tracks(artist).await
}

#[tauri::command]
async fn get_all_playlists(db_state: State<'_, DBState>) -> AnyResult<Vec<Playlist>> {
db_state.get_lock().await.get_all_playlists().await
Expand Down Expand Up @@ -432,6 +440,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
remove_tracks,
update_track,
get_artists,
get_artist_tracks,
get_all_playlists,
get_playlist,
get_playlist,
Expand Down
4 changes: 4 additions & 0 deletions src/components/TracksList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
position: relative;
}

.headless {
overflow-y: auto;
}

.tracksListRows {
width: 100%;
position: relative;
Expand Down
13 changes: 10 additions & 3 deletions src/components/TracksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ROW_HEIGHT_COMPACT = 24;
// --------------------------------------------------------------------------

type Props = {
type: string;
type: 'library' | 'playlist';
tracks: Track[];
tracksDensity: Config['track_view_density'];
trackPlayingID: string | null;
Expand All @@ -46,6 +46,7 @@ type Props = {
targetTrackID: string,
position: 'above' | 'below',
) => void;
headless?: boolean;
};

export default function TracksList(props: Props) {
Expand All @@ -58,6 +59,7 @@ export default function TracksList(props: Props) {
currentPlaylist,
onReorder,
playlists,
headless = false,
} = props;

const [selectedTracks, setSelectedTracks] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -449,8 +451,13 @@ export default function TracksList(props: Props) {
<div className={styles.tracksList}>
<Keybinding onKey={onKey} preventInputConflict />
{/* Scrollable element */}
<div ref={scrollableRef} className={styles.tracksListScroller}>
<TracksListHeader enableSort={type === 'library'} />
<div
ref={scrollableRef}
className={`${styles.tracksListScroller} ${styles.headless}`}
>
{headless === false && (
<TracksListHeader enableSort={type === 'library'} />
)}

{/* The large inner element to hold all of the items */}
<div
Expand Down
6 changes: 6 additions & 0 deletions src/generated/typings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ export type SortOrder = "Asc" | "Dsc";
* represent a single track, id and path should be unique
*/
export type Track = { id: string, path: string, title: string, album: string, artists: Array<string>, genres: Array<string>, year: number | null, duration: number, track_no: number | null, track_of: number | null, disk_no: number | null, disk_of: number | null, };

/**
* Represents a group of tracks, grouped by "something", lib artist name, or
* album name
*/
export type TrackGroup = { label: string, tracks: Array<Track>, };
55 changes: 54 additions & 1 deletion src/hooks/useFilteredTracks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useEffect, useMemo } from 'react';

import type { SortBy, SortOrder, Track } from '../generated/typings';
import type {
SortBy,
SortOrder,
Track,
TrackGroup,
} from '../generated/typings';
import {
filterTracks,
getSortOrder,
Expand Down Expand Up @@ -47,3 +52,51 @@ export default function useFilteredTracks(

return filteredTracks;
}

/**
* Filter and Sort a list of tracks groups depending on the user preferences and
* search
* IMPORTANT: can only be used ONCE per view, as it has side effects
*/
export function useFilteredTrackGroup(
tracks: TrackGroup[],
sortBy?: SortBy,
sortOrder?: SortOrder,
): TrackGroup[] {
const search = useLibraryStore((state) => stripAccents(state.search));
const libraryAPI = useLibraryAPI();

const filteredTrackGroup: TrackGroup[] = useMemo(() => {
let searchedGroups = tracks.map((group) => {
return {
label: group.label,
tracks: filterTracks(group.tracks, search),
};
});

if (sortBy && sortOrder) {
// sorting being a costly operation, do it after filtering, ignore it if not needed
searchedGroups = searchedGroups.map((group) => {
return {
label: group.label,
tracks: sortTracks(group.tracks, getSortOrder(sortBy), sortOrder),
};
});
}

return searchedGroups;
}, [tracks, search, sortBy, sortOrder]);

// Update the footer status based on the displayed tracks
useEffect(() => {
libraryAPI.setTracksStatus(
filteredTrackGroup.flatMap((group) => group.tracks),
);

return () => {
libraryAPI.setTracksStatus(null);
};
}, [filteredTrackGroup, libraryAPI.setTracksStatus]);

return filteredTrackGroup;
}
11 changes: 10 additions & 1 deletion src/lib/database.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { invoke } from '@tauri-apps/api/core';

import type { Playlist, ScanResult, Track } from '../generated/typings';
import type {
Playlist,
ScanResult,
Track,
TrackGroup,
} from '../generated/typings';

/**
* Bridge for the UI to communicate with the backend and manipulate the Database
Expand Down Expand Up @@ -42,6 +47,10 @@ const database = {
return invoke('plugin:database|get_artists');
},

async getArtistTracks(artist: string): Promise<Array<TrackGroup>> {
return invoke('plugin:database|get_artist_tracks', { artist });
},

// ---------------------------------------------------------------------------
// Playlists read/write actions
// ---------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions src/lib/typeguards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Track, TrackGroup } from '../generated/typings';

export function isTracksArray(array: Track[] | TrackGroup[]): array is Track[] {
return array.length === 0 || 'id' in array[0];
}
1 change: 1 addition & 0 deletions src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const routeTree: RouteObject[] = [
id: 'artist-details',
element: <ViewArtistDetails />,
loader: ViewArtistDetails.loader,
caseSensitive: true,
},
],
},
Expand Down
7 changes: 7 additions & 0 deletions src/routes/ViewArtistDetails.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.artist {
margin: 0;
}

.artistMetadata {
color: var(--text-muted);
}
56 changes: 51 additions & 5 deletions src/routes/artist-details.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
import { type LoaderFunctionArgs, useParams } from 'react-router-dom';
import {
type LoaderFunctionArgs,
useLoaderData,
useParams,
} from 'react-router-dom';
import TracksList from '../components/TracksList/TracksList';
import usePlayingTrackID from '../hooks/usePlayingTrackID';
import config from '../lib/config';
import database from '../lib/database';
import type { LoaderData } from '../types/museeks';

import { useMemo } from 'react';
import { useFilteredTrackGroup } from '../hooks/useFilteredTracks';
import { plural } from '../lib/localization';

import appStyles from './Root.module.css';
import styles from './ViewArtistDetails.module.css';

export default function ViewArtistDetails() {
// const { /** */ } = useLoaderData() as ArtistDetailsLoaderData;
const { albums, tracksDensity, playlists } =
useLoaderData() as ArtistDetailsLoaderData;
const params = useParams();
return <div>{params.artist}</div>;
const trackPlayingID = usePlayingTrackID();
const content = useFilteredTrackGroup(albums, 'Artist', 'Asc');

const tracksCount: number = useMemo(() => {
return content.map((album) => album.tracks.length).reduce((a, b) => a + b);
}, [content]);

return (
<div className={appStyles.view}>
<h1 className={styles.artist}>{params.artist}</h1>
<div className={styles.artistMetadata}>
{albums.length} {plural('album', albums.length)}, {tracksCount}{' '}
{plural('track', tracksCount)}
</div>

{/*
<TracksList
content={content}
type="library"
tracksDensity={tracksDensity}
trackPlayingID={trackPlayingID}
playlists={playlists}
headless
/> */}
</div>
);
}

export type ArtistDetailsLoaderData = LoaderData<
Expand All @@ -15,6 +56,11 @@ ViewArtistDetails.loader = async ({ params }: LoaderFunctionArgs) => {
if (typeof params.artist !== 'string') {
throw new Error('Invalid artist');
}

return {};
return {
albums: await database.getArtistTracks(params.artist),
playlists: await database.getAllPlaylists(),
tracksDensity: (await config.get('track_view_density')) as
| 'compact'
| 'normal',
};
};

0 comments on commit 34d4941

Please sign in to comment.