diff --git a/package.json b/package.json index 3b0e1f7..212f623 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@egjs/react-flicking": "^4.11.0", "@egjs/react-grid": "^1.15.2", + "@faker-js/faker": "^8.3.1", "@hookform/resolvers": "^3.1.1", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", @@ -56,8 +57,8 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.45.2", "react-toast": "^1.0.3", + "react-viewport-list": "^7.1.1", "react-virtualized": "^9.22.5", - "react-window": "^1.8.9", "react-window-infinite-loader": "^1.0.9", "shadcn-ui": "^0.3.0", "tailwind-merge": "^1.13.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6f4215..424ea5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@egjs/react-grid': specifier: ^1.15.2 version: 1.15.2 + '@faker-js/faker': + specifier: ^8.3.1 + version: 8.3.1 '@hookform/resolvers': specifier: ^3.1.1 version: 3.1.1(react-hook-form@7.45.2) @@ -125,12 +128,12 @@ dependencies: react-toast: specifier: ^1.0.3 version: 1.0.3(react@18.2.0) + react-viewport-list: + specifier: ^7.1.1 + version: 7.1.1(react@18.2.0) react-virtualized: specifier: ^9.22.5 version: 9.22.5(react-dom@18.2.0)(react@18.2.0) - react-window: - specifier: ^1.8.9 - version: 1.8.9(react-dom@18.2.0)(react@18.2.0) react-window-infinite-loader: specifier: ^1.0.9 version: 1.0.9(react-dom@18.2.0)(react@18.2.0) @@ -733,6 +736,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@faker-js/faker@8.3.1: + resolution: {integrity: sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + dev: false + /@floating-ui/core@1.3.1: resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==} dev: false @@ -5590,10 +5598,6 @@ packages: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true - /memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - dev: false - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6601,6 +6605,14 @@ packages: react: 18.2.0 dev: false + /react-viewport-list@7.1.1(react@18.2.0): + resolution: {integrity: sha512-O3gxykg3DgpcyYH+/X2kFwWFaZjckJ0FK3UBb4vFhZe+CsGLcX8OVJb0VSYI+IupEuQ9pl8dvJak8JRkIuvNjw==} + peerDependencies: + react: '>=17.0.0' + dependencies: + react: 18.2.0 + dev: false + /react-virtual@2.10.4(react@18.2.0): resolution: {integrity: sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==} peerDependencies: @@ -6637,19 +6649,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-window@1.8.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.22.6 - memoize-one: 5.2.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cb109af..bd321bb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -110,6 +111,7 @@ dependencies = [ "env_logger", "file-format", "futures", + "indicium", "lofty", "log", "rand 0.8.5", @@ -959,6 +961,12 @@ dependencies = [ "time 0.3.23", ] +[[package]] +name = "eddie" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ad36183b190ad1632999a8735b9842f3fda7a5afa433d9132e80860ef7051a" + [[package]] name = "embed-resource" version = "2.2.0" @@ -1856,6 +1864,18 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "indicium" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8e0829bc7bb361e0418535075a519519361022d691077e0dd4d0dc8c2afc5" +dependencies = [ + "ahash", + "eddie", + "kstring", + "tracing", +] + [[package]] name = "infer" version = "0.12.0" @@ -2001,6 +2021,15 @@ dependencies = [ "treediff", ] +[[package]] +name = "kstring" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" +dependencies = [ + "static_assertions", +] + [[package]] name = "kuchiki" version = "0.8.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d210b70..424608d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,6 +33,7 @@ futures = "0.3.29" youtube_dl = { version = "0.9.0", features = ["tokio"] } tokio = { version = "1.20.1", features = ["full"] } base64 = { version = "0.21.5" } +indicium = "0.6.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/api/player.rs b/src-tauri/src/api/player.rs index 6c31122..38f8ee1 100644 --- a/src-tauri/src/api/player.rs +++ b/src-tauri/src/api/player.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, Mutex}, }; +use indicium::simple::SearchIndex; use log::{error, info}; use tauri::{AppHandle, State}; @@ -95,3 +96,30 @@ pub async fn seek_to( Err(e) => Err(e.to_string()), } } + +#[tauri::command] +pub async fn search_audio( + query: String, + playlist: String, + player: State<'_, Arc>>, +) -> Result, String> { + let player = player.lock().unwrap(); + if query.is_empty() { + return Ok(super::utils::create_audio_list(player, &playlist)); + } + let mut results = Vec::new(); + let mut search_index: SearchIndex = SearchIndex::default(); + + // this has to be done when the app starts ! + // this create a problem when we switch between playlist and search + player.playlists[&playlist] + .iter() + .enumerate() + .for_each(|(i, audio)| search_index.insert(&i, audio)); + + let search_results = search_index.search(&query); + for result in search_results { + results.push(super::utils::create_audio(&player.audios[*result], result)); + } + Ok(results) +} diff --git a/src-tauri/src/api/utils.rs b/src-tauri/src/api/utils.rs index 4e1f0eb..f15c3eb 100644 --- a/src-tauri/src/api/utils.rs +++ b/src-tauri/src/api/utils.rs @@ -29,29 +29,25 @@ pub fn create_audio_list(player: MutexGuard<'_, MusicPlayer>, str: &str) -> Vec< let mut audios = Vec::new(); if !str.is_empty() { for (id, audio) in player.playlists[str].iter().enumerate() { - audios.push(Audio { - path: audio.path.clone(), - title: audio.tag.title.clone(), - artist: audio.tag.artist.clone(), - album: audio.tag.album.clone(), - duration: audio.duration.as_secs(), - id, - cover: audio.cover.clone(), - }); + audios.push(create_audio(audio, &id)); } } else { for (id, audio) in player.audios.iter().enumerate() { - audios.push(Audio { - path: audio.path.clone(), - title: audio.tag.title.clone(), - artist: audio.tag.artist.clone(), - album: audio.tag.album.clone(), - duration: audio.duration.as_secs(), - id, - cover: audio.cover.clone(), - }); + audios.push(create_audio(audio, &id)); } } audios } + +pub fn create_audio(audio: &crate::music::audio::_Audio, id: &usize) -> Audio { + Audio { + path: audio.path.clone(), + title: audio.tag.title.clone(), + artist: audio.tag.artist.clone(), + album: audio.tag.album.clone(), + duration: audio.duration.as_secs(), + id: *id, + cover: audio.cover.clone(), + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c23a394..d3f42ba 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -78,6 +78,7 @@ fn main() { player::pause, player::update_player, player::seek_to, + player::search_audio, audio::updated_current_playlist, audio::retrieve_audios, audio::current_audio_status, diff --git a/src-tauri/src/music/audio.rs b/src-tauri/src/music/audio.rs index 0729614..1017a28 100644 --- a/src-tauri/src/music/audio.rs +++ b/src-tauri/src/music/audio.rs @@ -142,6 +142,19 @@ impl std::cmp::PartialEq for _Audio { } } +struct AudioWrapper(_Audio); + +impl indicium::simple::Indexable for AudioWrapper { + fn strings(&self) -> Vec { + vec![ + self.0.path.clone(), + duration_to_string(self.0.duration), + self.0.format.clone(), + self.0.status.to_string(), + ] + } +} + fn gen_tag(path: &PathBuf) -> TaggedFile { let tagged_file = Probe::open(path); match tagged_file { diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 6a4f68a..b95313a 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -63,8 +63,8 @@ export async function update_after_play( const playlist = isObjectEmpty(currentPlaylistListening as unknown as object) ? name : !fromMusicPage - ? currentPlaylistListening - : name + ? currentPlaylistListening + : name console.log(playlist) await invoke("update_player", { playlist: playlist, @@ -171,6 +171,7 @@ export function Player() { total: 0, status: "stopped", } as AudioStatus) + // the audio should be retrieved from backend and the not the context const { audio } = useContext(AppContext) const wupdate_status = () => { void invoke("current_audio_status") @@ -202,7 +203,7 @@ export function Player() { }, [wupdate_status, status]) useEffect(() => { - ;(() => { + ; (() => { if (!isObjectEmpty(audio)) { invoke("update_history") .then(() => { diff --git a/src/components/ui/MusicCard.tsx b/src/components/ui/MusicCard.tsx index c8c257f..2d7421a 100644 --- a/src/components/ui/MusicCard.tsx +++ b/src/components/ui/MusicCard.tsx @@ -47,7 +47,7 @@ export default function MusicCard({ await play(context, audio, true) }} id={`audio-${audio.id}`} - className="hover:cursor-pointer p-4 rounded-lg transition ease-in-out delay-90 dark:hover:bg-gray-900 hover:bg-gray-50 duration-150 flex items-center space-x-8 w-full" + className="hover:cursor-pointer p-4 rounded-lg transition ease-in-out delay-90 dark:hover:bg-gray-900 hover:bg-gray-50 duration-150 flex items-center space-x-6 w-full" >
(({ className, ...props }, ref) => ( +)) + +// eslint-disable-next-line react/display-name +const CommandMusicList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + )) @@ -118,6 +130,14 @@ const CommandItem = React.forwardRef< /> )) +// eslint-disable-next-line react/display-name +const CommandMusicItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + CommandItem.displayName = CommandPrimitive.Item.displayName const CommandShortcut = ({ @@ -141,6 +161,8 @@ export { CommandInput, CommandItem, CommandList, + CommandMusicItem, + CommandMusicList, CommandSeparator, CommandShortcut, } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 5b51786..20f161a 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,3 +1,4 @@ +import { Search } from "lucide-react" import * as React from "react" import { cn } from "@/lib/utils" @@ -19,6 +20,29 @@ const Input = React.forwardRef( ) }, ) + +const SearchInput = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +
+ + +
+ ) + }, +) + Input.displayName = "Input" +SearchInput.displayName = "SearchInput" -export { Input } +export { Input, SearchInput } diff --git a/src/pages/music.tsx b/src/pages/music.tsx index 1c392d0..a405791 100644 --- a/src/pages/music.tsx +++ b/src/pages/music.tsx @@ -1,25 +1,33 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable react/display-name */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-misused-promises */ +import { invoke } from "@tauri-apps/api/tauri" import { /*Book,*/ PenBox, Shuffle } from "lucide-react" import Image from "next/image" import { useContext, useEffect } from "react" +// import { ViewportList } from 'react-viewport-list' import { List } from "react-virtualized" -import { AppContext } from "@/components/AppContext" +import { AppContext, appContext } from "@/components/AppContext" import { fetchPlaylistCheckedState, setAudiosFromPlaylist, } from "@/components/contexts_menu/CPlaylistSub" import { shuffle } from "@/components/player/Player" +import { Audio } from "@/components/types/audio" import { Button } from "@/components/ui/button" +import { SearchInput } from "@/components/ui/input" import MusicCard from "@/components/ui/MusicCard" import { b64imageWrap, cn, isObjectEmpty } from "@/lib/utils" + export default function Music({ name }: { name: string }) { const context = useContext(AppContext) - useEffect(() => { setAudiosFromPlaylist(name, context.setAudioList) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -78,33 +86,41 @@ export default function Music({ name }: { name: string }) {
{/* 2/4 */} -
-
- {/* {context.audioList.map((value, index) => { - // needs to be lazy loaded - return - })} */} - {/* Not reponsive :( */} +
+ {/* Not reponsive :( */} +
+ { + const inputValue = value.target.value.toLowerCase(); + invoke("search_audio", { query: inputValue, playlist: name }).then((filteredAudioList) => { + context.setAudioList(filteredAudioList) + }).catch(console.error) + }} /> + + {/* {context.audioList.map((value, index) => { + // needs to be lazy loaded + return ( + ) + })} */}
-
+
) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function rowRenderer({ key, style }: any) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unused-vars - const index = key.split("-")[0] - return ( -
- -
- ) - } } + +const rowRenderer = + (context: appContext, name: string) => + ({ index, style }: any) => { + + return ( +
+ +
+ ) + } \ No newline at end of file