Skip to content

Commit

Permalink
wip details
Browse files Browse the repository at this point in the history
  • Loading branch information
martpie committed Dec 4, 2024
1 parent d8b7151 commit cec92cf
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 19 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 @@ -32,6 +32,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 @@ -293,6 +293,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 @@ -430,6 +438,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 @@ -451,8 +453,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;
}
19 changes: 19 additions & 0 deletions src/lib/__tests__/localization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, test } from 'bun:test';
import { plural } from '../localization';

describe('localization', () => {
describe('plural', () => {
test("should add a 's' to strings with a count of 0 or 2 or more", () => {
expect(plural('album', 0)).toEqual('albums');
expect(plural('album', 2)).toEqual('albums');
expect(plural('album', 3)).toEqual('albums');
expect(plural('album', 10)).toEqual('albums');
expect(plural('track', 10)).toEqual('tracks');
});

test("should not add a 's' to strings with a count of 1", () => {
expect(plural('album', 1)).toEqual('album');
expect(plural('track', 1)).toEqual('track');
});
});
});
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
6 changes: 6 additions & 0 deletions src/lib/localization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Pluralize a word
*/
export function plural(str: string, count: number) {
return count === 1 ? str : `${str}s`;
}
4 changes: 3 additions & 1 deletion src/lib/route-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RouteObject } from 'react-router-dom';
import GlobalErrorBoundary from '../components/GlobalErrorBoundary';
import RootView from '../views/Root';
import ViewArtistDetails from '../views/ViewArtistDetails';
Expand All @@ -12,7 +13,7 @@ import ViewSettingsLibrary from '../views/ViewSettingsLibrary';
import ViewSettingsUI from '../views/ViewSettingsUI';
import ViewTrackDetails from '../views/ViewTrackDetails';

const routeTree = [
const routeTree: RouteObject[] = [
{
path: '/',
id: 'root',
Expand Down Expand Up @@ -51,6 +52,7 @@ const routeTree = [
id: 'artist-details',
element: <ViewArtistDetails />,
loader: ViewArtistDetails.loader,
caseSensitive: true,
},
],
},
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];
}
7 changes: 7 additions & 0 deletions src/views/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);
}
Loading

0 comments on commit cec92cf

Please sign in to comment.