[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Migrate to napi v2 macros

Kasper (Oct 12, 2022, 7:29 AM +0200) ad22d1c7 95254a23

+443 -766
+1 -1
Cargo.toml
··· 14 14 napi-build = "2.0" 15 15 16 16 [dependencies] 17 - napi = { version = "2.9", features = ["serde-json", "napi4"] } 17 + napi = { version = "2.9", features = ["serde-json", "napi6"] } 18 18 napi-derive = "2.9" 19 19 dirs = "4.0" 20 20 serde = { version = "1.0", features = ["derive"] }
+43 -44
src-native/data.rs
··· 59 59 None => &self.open_playlist_track_ids, 60 60 } 61 61 } 62 - } 62 + pub fn load(is_dev: bool) -> UniResult<Data> { 63 + let paths = if is_dev { 64 + let appdata_dir = env::current_dir().unwrap().join("appdata"); 65 + let library_dir = appdata_dir.join("Library"); 66 + Paths { 67 + library_dir: library_dir.clone(), 68 + tracks_dir: library_dir.join("Tracks"), 69 + library_json: library_dir.join("Library.json"), 70 + local_data_dir: dirs::data_local_dir() 71 + .ok_or("Local data folder not found")? 72 + .join("space.kasper.ferrum.dev"), 73 + } 74 + } else { 75 + let music_dir = dirs::audio_dir().ok_or("Music folder not found")?; 76 + let library_dir = music_dir.join("Ferrum"); 77 + Paths { 78 + library_dir: library_dir.clone(), 79 + tracks_dir: library_dir.join("Tracks"), 80 + library_json: library_dir.join("Library.json"), 81 + local_data_dir: dirs::data_local_dir() 82 + .ok_or("Local data folder not found")? 83 + .join("space.kasper.ferrum"), 84 + } 85 + }; 63 86 64 - pub fn load_data(is_dev: bool) -> UniResult<Data> { 65 - let paths = if is_dev { 66 - let appdata_dir = env::current_dir().unwrap().join("appdata"); 67 - let library_dir = appdata_dir.join("Library"); 68 - Paths { 69 - library_dir: library_dir.clone(), 70 - tracks_dir: library_dir.join("Tracks"), 71 - library_json: library_dir.join("Library.json"), 72 - local_data_dir: dirs::data_local_dir() 73 - .ok_or("Local data folder not found")? 74 - .join("space.kasper.ferrum.dev"), 75 - } 76 - } else { 77 - let music_dir = dirs::audio_dir().ok_or("Music folder not found")?; 78 - let library_dir = music_dir.join("Ferrum"); 79 - Paths { 80 - library_dir: library_dir.clone(), 81 - tracks_dir: library_dir.join("Tracks"), 82 - library_json: library_dir.join("Library.json"), 83 - local_data_dir: dirs::data_local_dir() 84 - .ok_or("Local data folder not found")? 85 - .join("space.kasper.ferrum"), 86 - } 87 - }; 87 + let loaded_library = load_library(&paths); 88 + let loaded_cache = SidebarView::load(&paths); 88 89 89 - let loaded_library = load_library(&paths); 90 - let loaded_cache = SidebarView::load(&paths); 91 - 92 - let mut data = Data { 93 - is_dev, 94 - paths, 95 - library: loaded_library, 96 - view_cache: loaded_cache, 97 - open_playlist_id: "root".to_string(), 98 - open_playlist_track_ids: vec![], 99 - page_track_ids: None, 100 - filter: "".to_string(), 101 - sort_key: "index".to_string(), 102 - sort_desc: true, 103 - current_tag: None, 104 - }; 105 - data.open_playlist_track_ids = page::get_track_ids(&data)?; 106 - sort(&mut data, "dateAdded", true)?; 107 - return Ok(data); 90 + let mut data = Data { 91 + is_dev, 92 + paths, 93 + library: loaded_library, 94 + view_cache: loaded_cache, 95 + open_playlist_id: "root".to_string(), 96 + open_playlist_track_ids: vec![], 97 + page_track_ids: None, 98 + filter: "".to_string(), 99 + sort_key: "index".to_string(), 100 + sort_desc: true, 101 + current_tag: None, 102 + }; 103 + data.open_playlist_track_ids = page::get_track_ids(&data)?; 104 + sort(&mut data, "dateAdded", true)?; 105 + return Ok(data); 106 + } 108 107 }
+27 -99
src-native/data_js.rs
··· 1 - use crate::data::{load_data, Data}; 2 - use crate::js::arg_to_bool; 3 - use crate::sidebar_view; 4 - use crate::{filter, page, playlists, tracks}; 5 - use napi::{CallContext, Env, JsObject, JsUndefined, JsUnknown, Result as NResult, Task}; 6 - use napi_derive::js_function; 1 + use crate::data::Data; 2 + use napi::{Env, JsUndefined, JsUnknown, Result}; 7 3 8 - pub fn get_data<'a>(ctx: &'a CallContext) -> NResult<&'a mut Data> { 9 - let this: JsObject = ctx.this()?; 10 - let data: &mut Data = ctx.env.unwrap(&this)?; 4 + pub fn get_data<'a>(env: &'a Env) -> Result<&'a mut Data> { 5 + let data = env.get_instance_data::<Data>()?.ok_or(nerr!("No data"))?; 11 6 return Ok(data); 12 7 } 13 8 14 - #[js_function(1)] 15 - pub fn load_data_js(ctx: CallContext) -> NResult<JsObject> { 16 - let is_dev = arg_to_bool(&ctx, 0)?; 17 - 18 - let data: Data = load_data(is_dev)?; 19 - 20 - let mut new_this: JsObject = ctx.env.create_object()?; 21 - ctx.env.wrap(&mut new_this, data)?; 22 - new_this = init_data_instance(new_this)?; 23 - return Ok(new_this); 9 + #[napi(js_name = "load_data")] 10 + #[allow(dead_code)] 11 + pub fn load_data(is_dev: bool, env: Env) -> Result<()> { 12 + let data = Data::load(is_dev)?; 13 + env.set_instance_data(data, 0, |_ctx| {})?; 14 + return Ok(()); 24 15 } 25 16 26 - struct LoadData(bool); 27 - impl Task for LoadData { 28 - type Output = Data; 29 - type JsValue = JsObject; 30 - fn compute(&mut self) -> NResult<Self::Output> { 31 - let is_dev = self.0; 32 - let data: Data = load_data(is_dev)?; 33 - return Ok(data); 34 - } 35 - fn resolve(&mut self, env: Env, output: Self::Output) -> NResult<Self::JsValue> { 36 - let mut new_this: JsObject = env.create_object()?; 37 - env.wrap(&mut new_this, output)?; 38 - new_this = init_data_instance(new_this)?; 39 - return Ok(new_this); 40 - } 41 - } 42 - #[js_function(1)] 43 - pub fn load_data_async(ctx: CallContext) -> NResult<JsObject> { 44 - let is_dev = arg_to_bool(&ctx, 0)?; 45 - 46 - let data: Data = load_data(is_dev)?; 47 - 48 - let mut new_this: JsObject = ctx.env.create_object()?; 49 - ctx.env.wrap(&mut new_this, data)?; 50 - new_this = init_data_instance(new_this)?; 51 - return Ok(new_this); 17 + #[napi(object)] 18 + pub struct PathsJs { 19 + pub library_dir: String, 20 + pub tracks_dir: String, 21 + pub library_json: String, 22 + pub local_data_dir: String, 52 23 } 53 24 54 - #[js_function(0)] 55 - pub fn get_paths(ctx: CallContext) -> NResult<JsUnknown> { 56 - let data: &mut Data = get_data(&ctx)?; 57 - let js_obj = ctx.env.to_js_value(&data.paths); 58 - return js_obj; 25 + #[napi(js_name = "get_paths", ts_return_type = "PathsJs")] 26 + #[allow(dead_code)] 27 + pub fn get_paths(env: Env) -> Result<JsUnknown> { 28 + let data: &mut Data = get_data(&env)?; 29 + let paths = env.to_js_value(&data.paths)?; 30 + Ok(paths) 59 31 } 60 32 61 - #[js_function(0)] 62 - pub fn save(ctx: CallContext) -> NResult<JsUndefined> { 63 - let data: &mut Data = get_data(&ctx)?; 33 + #[napi(js_name = "save")] 34 + #[allow(dead_code)] 35 + pub fn save(env: Env) -> Result<JsUndefined> { 36 + let data: &mut Data = get_data(&env)?; 64 37 data.save()?; 65 - return ctx.env.get_undefined(); 66 - } 67 - 68 - fn init_data_instance(mut exports: JsObject) -> NResult<JsObject> { 69 - exports.create_named_method("get_paths", get_paths)?; 70 - exports.create_named_method("save", save)?; 71 - 72 - exports.create_named_method("import_track", tracks::import)?; 73 - exports.create_named_method("get_track", tracks::get_track)?; 74 - exports.create_named_method("add_play", tracks::add_play)?; 75 - exports.create_named_method("add_skip", tracks::add_skip)?; 76 - exports.create_named_method("add_play_time", tracks::add_play_time)?; 77 - exports.create_named_method("read_cover_async", tracks::read_cover_async)?; 78 - exports.create_named_method("update_track_info", tracks::update_track_info)?; 79 - exports.create_named_method("load_tags", tracks::load_tags)?; 80 - exports.create_named_method("get_image", tracks::get_image)?; 81 - exports.create_named_method("set_image", tracks::set_image)?; 82 - exports.create_named_method("set_image_data", tracks::set_image_data)?; 83 - exports.create_named_method("remove_image", tracks::remove_image)?; 84 - 85 - exports.create_named_method("get_track_lists", playlists::get_track_lists)?; 86 - exports.create_named_method("playlist_filter_duplicates", playlists::filter_duplicates)?; 87 - exports.create_named_method("add_tracks_to_playlist", playlists::add_tracks)?; 88 - exports.create_named_method("remove_from_open_playlist", playlists::remove_from_open)?; 89 - exports.create_named_method("delete_tracks_in_open", playlists::delete_tracks_in_open)?; 90 - exports.create_named_method("new_playlist", playlists::new_playlist)?; 91 - exports.create_named_method("update_playlist", playlists::update_playlist)?; 92 - exports.create_named_method("move_playlist", playlists::move_playlist)?; 93 - 94 - exports.create_named_method("refresh_page", page::refresh)?; 95 - exports.create_named_method("open_playlist", page::open_playlist)?; 96 - exports.create_named_method("get_page_track_ids", page::get_page_track_ids)?; 97 - exports.create_named_method("filter_open_playlist", filter::filter_js)?; 98 - exports.create_named_method("get_page_track", page::get_page_track)?; 99 - exports.create_named_method("get_page_track_id", page::get_page_track_id)?; 100 - exports.create_named_method("get_page_info", page::get_page_info)?; 101 - exports.create_named_method("sort", page::sort_js)?; 102 - exports.create_named_method("move_tracks", page::move_tracks)?; 103 - 104 - exports.create_named_method( 105 - "shown_playlist_folders", 106 - sidebar_view::shown_playlist_folders, 107 - )?; 108 - exports.create_named_method("view_folder_set_show", sidebar_view::view_folder_set_show)?; 109 - 110 - Ok(exports) 38 + return env.get_undefined(); 111 39 }
+6 -8
src-native/filter.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 - use crate::js::arg_to_string; 4 3 use crate::library_types::TrackID; 5 - use napi::{CallContext, JsUndefined, Result as NResult}; 6 - use napi_derive::js_function; 4 + use napi::{Env, Result}; 7 5 use std::str::Chars; 8 6 use std::time::Instant; 9 7 10 - #[js_function(1)] 11 - pub fn filter_js(ctx: CallContext) -> NResult<JsUndefined> { 12 - let data: &mut Data = get_data(&ctx)?; 13 - let query = arg_to_string(&ctx, 0)?; 8 + #[napi(js_name = "filter_open_playlist")] 9 + #[allow(dead_code)] 10 + pub fn filter_js(query: String, env: Env) -> Result<()> { 11 + let data: &mut Data = get_data(&env)?; 14 12 filter(data, query); 15 - return ctx.env.get_undefined(); 13 + return Ok(()); 16 14 } 17 15 18 16 fn match_at_start(mut haystack: Chars, needle: Chars) -> bool {
+1 -79
src-native/js.rs
··· 1 - use crate::data_js::{load_data_async, load_data_js}; 2 1 use atomicwrites::{AllowOverwrite, AtomicFile}; 3 - use napi::{ 4 - CallContext, Error as NapiError, JsArrayBuffer, JsBoolean, JsNumber, JsObject, JsString, 5 - JsTypedArray, Result as NResult, 6 - }; 7 - use napi_derive::module_exports; 8 - use std::convert::TryFrom; 9 - use std::convert::TryInto; 2 + use napi::Error as NapiError; 10 3 use std::fs::copy; 11 4 use std::io::Write; 12 5 ··· 15 8 return NapiError::from_reason(msg.to_owned()); 16 9 } 17 10 18 - pub fn arg_to_string(ctx: &CallContext, arg: usize) -> NResult<String> { 19 - let js_string: JsString = ctx.get(arg)?; 20 - let js_utf8_string = js_string.into_utf8()?; 21 - let rust_string = js_utf8_string.as_str()?.to_string(); 22 - return Ok(rust_string); 23 - } 24 - 25 - pub fn arg_to_bool(ctx: &CallContext, arg: usize) -> NResult<bool> { 26 - let js_bool: JsBoolean = ctx.get(arg)?; 27 - let rust_bool = js_bool.get_value()?; 28 - return Ok(rust_bool); 29 - } 30 - 31 - pub fn arg_to_bytes(ctx: &CallContext, arg: usize) -> NResult<Vec<u8>> { 32 - let js_array_buffer: JsArrayBuffer = ctx.get(arg)?; 33 - let buf = js_array_buffer.into_value()?; 34 - let bytes: Vec<u8> = buf.to_vec(); 35 - return Ok(bytes); 36 - } 37 - 38 - pub fn arg_to_number<N: TryFrom<JsNumber, Error = napi::Error>>( 39 - ctx: &CallContext, 40 - arg: usize, 41 - ) -> NResult<N> { 42 - let js_number: JsNumber = ctx.get(arg)?; 43 - let number: N = match js_number.try_into() { 44 - Ok(n) => n, 45 - Err(err) => return Err(err), 46 - }; 47 - return Ok(number); 48 - } 49 - 50 - pub fn arg_to_number_vector<N: TryFrom<JsNumber, Error = napi::Error>>( 51 - ctx: &CallContext, 52 - arg: usize, 53 - ) -> NResult<Vec<N>> { 54 - let js_array: JsTypedArray = ctx.get(arg)?; 55 - let length: u32 = js_array.get_array_length()?; 56 - let mut vector: Vec<N> = Vec::new(); 57 - for i in 0..length { 58 - let js_number: JsNumber = js_array.get_element(i)?; 59 - match js_number.try_into() { 60 - Ok(n) => vector.push(n), 61 - Err(err) => return Err(err), 62 - }; 63 - } 64 - return Ok(vector); 65 - } 66 - 67 - pub fn arg_to_string_vector(ctx: &CallContext, arg: usize) -> NResult<Vec<String>> { 68 - let js_array: JsTypedArray = ctx.get(arg)?; 69 - let length: u32 = js_array.get_array_length()?; 70 - let mut vector: Vec<String> = Vec::new(); 71 - for i in 0..length { 72 - let js_string: JsString = js_array.get_element(i)?; 73 - let js_utf8_string = js_string.into_utf8()?; 74 - let rust_string = js_utf8_string.as_str()?.to_string(); 75 - vector.push(rust_string); 76 - } 77 - return Ok(vector); 78 - } 79 - 80 11 #[napi] 81 12 #[allow(dead_code)] 82 13 fn copy_file(from: String, to: String) -> napi::Result<()> { ··· 92 23 let af = AtomicFile::new(file_path, AllowOverwrite); 93 24 af.write(|f| f.write_all(content.as_bytes())).unwrap(); 94 25 } 95 - 96 - #[module_exports] 97 - fn init(mut exports: JsObject) -> NResult<()> { 98 - // exports.create_named_method("copy_file", copy_file)?; 99 - // exports.create_named_method("atomic_file_save", atomic_file_save)?; 100 - exports.create_named_method("load_data", load_data_js)?; 101 - exports.create_named_method("load_data_async", load_data_async)?; 102 - Ok(()) 103 - }
+14 -7
src-native/library_types.rs
··· 3 3 use crate::{get_now_timestamp, UniResult}; 4 4 use linked_hash_map::LinkedHashMap; 5 5 use nanoid::nanoid; 6 + use napi::bindgen_prelude::ToNapiValue; 6 7 use serde::{Deserialize, Serialize}; 7 8 use serde_repr::{Deserialize_repr, Serialize_repr}; 8 9 ··· 120 121 /// (track id, start time, duration) 121 122 pub type PlayTime = (TrackID, MsSinceUnixEpoch, i64); 122 123 123 - #[derive(Serialize, Deserialize, Debug)] 124 + #[derive(Serialize, Deserialize, Clone, Debug)] 125 + #[napi(object)] 124 126 pub struct Track { 125 127 pub size: i64, 126 128 pub duration: f64, ··· 132 134 pub name: String, 133 135 #[serde(default, skip_serializing_if = "Option::is_none")] 134 136 pub importedFrom: Option<String>, 135 - /// Imported ID, like iTunes Persistent ID: 137 + /// Imported ID, like iTunes Persistent ID 136 138 #[serde(default, skip_serializing_if = "Option::is_none")] 137 139 pub originalId: Option<String>, 138 140 #[serde(default)] ··· 199 201 pub volume: Option<PercentInteger>, 200 202 } 201 203 202 - #[derive(Serialize, Deserialize, Debug)] 204 + #[derive(Serialize, Deserialize, Clone, Debug)] 205 + #[napi(object)] 203 206 pub struct CountObject { 204 - count: i64, 205 - fromDate: MsSinceUnixEpoch, 206 - toDate: MsSinceUnixEpoch, 207 + pub count: i64, 208 + pub fromDate: MsSinceUnixEpoch, 209 + pub toDate: MsSinceUnixEpoch, 207 210 } 208 211 209 212 #[derive(Serialize, Deserialize, Clone, Debug)] ··· 228 231 } 229 232 230 233 #[derive(Serialize, Deserialize, Clone, Debug)] 234 + #[napi(object)] 231 235 pub struct Playlist { 232 236 pub id: TrackListID, 233 237 pub name: String, ··· 249 253 } 250 254 251 255 #[derive(Serialize, Deserialize, Clone, Debug)] 256 + #[napi(object)] 252 257 pub struct Folder { 253 258 pub id: TrackListID, 254 259 pub name: String, ··· 272 277 } 273 278 274 279 #[derive(Serialize, Deserialize, Clone, Debug)] 280 + #[napi(object)] 275 281 pub struct Special { 276 282 pub id: TrackListID, 277 283 pub name: SpecialTrackListName, ··· 279 285 pub children: Vec<TrackListID>, 280 286 } 281 287 282 - #[derive(Serialize, Deserialize, Clone, Debug)] 288 + #[derive(Serialize, Deserialize, Debug)] 283 289 #[non_exhaustive] 290 + #[napi] 284 291 pub enum SpecialTrackListName { 285 292 Root, 286 293 }
+89 -61
src-native/page.rs
··· 1 - use std::collections::HashSet; 2 - use std::time::Instant; 3 - 4 1 use crate::data::Data; 5 2 use crate::data_js::get_data; 6 - use crate::js::{arg_to_bool, arg_to_number, arg_to_number_vector, arg_to_string, nerr}; 3 + use crate::js::nerr; 7 4 use crate::library::{get_track_field_type, TrackField}; 8 - use crate::library_types::{SpecialTrackListName, TrackID, TrackList}; 5 + use crate::library_types::{SpecialTrackListName, Track, TrackID, TrackList}; 9 6 use crate::sort::sort; 10 7 use crate::{filter, UniResult}; 11 - use napi::{CallContext, JsObject, JsString, JsUndefined, JsUnknown, Result as NResult}; 12 - use napi_derive::js_function; 8 + use napi::{Env, JsString, JsUndefined, JsUnknown, Result}; 9 + use std::collections::HashSet; 10 + use std::time::Instant; 13 11 14 - #[js_function(1)] 15 - pub fn open_playlist(ctx: CallContext) -> NResult<JsUndefined> { 16 - let data: &mut Data = get_data(&ctx)?; 17 - data.open_playlist_id = arg_to_string(&ctx, 0)?; 12 + #[napi(js_name = "open_playlist")] 13 + #[allow(dead_code)] 14 + pub fn open_playlist(open_playlist_id: String, env: Env) -> Result<JsUndefined> { 15 + let data: &mut Data = get_data(&env)?; 16 + data.open_playlist_id = open_playlist_id; 18 17 data.open_playlist_track_ids = get_track_ids(&data)?; 19 18 data.page_track_ids = None; 20 19 match data.library.get_tracklist(&data.open_playlist_id)? { ··· 26 25 data.sort_desc = true; 27 26 } 28 27 }; 29 - return ctx.env.get_undefined(); 28 + return env.get_undefined(); 30 29 } 31 30 32 - #[js_function(1)] 33 - pub fn get_page_track(ctx: CallContext) -> NResult<JsUnknown> { 34 - let data: &mut Data = get_data(&ctx)?; 35 - let index: i64 = arg_to_number(&ctx, 0)?; 31 + #[napi(js_name = "get_page_track")] 32 + #[allow(dead_code)] 33 + pub fn get_page_track(index: i64, env: Env) -> Result<Track> { 34 + let data: &mut Data = get_data(&env)?; 36 35 let page_track_ids = data.get_page_tracks(); 37 36 let track_id = page_track_ids.get(index as usize).ok_or(nerr!( 38 37 "Track index {} not found in open playlist", 39 38 index.to_string() 40 39 ))?; 41 - let tracks = &data.library.tracks; 42 - let track = tracks.get(track_id).ok_or(nerr("Track ID not found"))?; 43 - let js = ctx.env.to_js_value(track)?; 44 - return Ok(js); 40 + let track = data 41 + .library 42 + .tracks 43 + .get(track_id) 44 + .ok_or(nerr("Track ID not found"))?; 45 + return Ok(track.clone()); 45 46 } 46 47 47 - #[js_function(1)] 48 - pub fn get_page_track_id(ctx: CallContext) -> NResult<JsString> { 49 - let data: &mut Data = get_data(&ctx)?; 50 - let index: i64 = arg_to_number(&ctx, 0)?; 48 + #[napi(js_name = "get_page_track_id")] 49 + #[allow(dead_code)] 50 + pub fn get_page_track_id(index: i64, env: Env) -> Result<JsString> { 51 + let data: &mut Data = get_data(&env)?; 51 52 let page_track_ids = data.get_page_tracks(); 52 53 let track_id = page_track_ids.get(index as usize).ok_or(nerr!( 53 54 "Track index {} not found in open playlist", 54 55 index.to_string() 55 56 ))?; 56 - return ctx.env.create_string(track_id); 57 + return env.create_string(track_id); 57 58 } 58 59 59 - #[js_function(0)] 60 - pub fn refresh(ctx: CallContext) -> NResult<JsUndefined> { 61 - let data: &mut Data = get_data(&ctx)?; 60 + #[napi(js_name = "refresh_page")] 61 + #[allow(dead_code)] 62 + pub fn refresh(env: Env) -> Result<JsUndefined> { 63 + let data: &mut Data = get_data(&env)?; 62 64 let sort_key = data.sort_key.clone(); 63 65 let sort_desc = data.sort_desc.clone(); 64 66 sort(data, &sort_key, sort_desc)?; 65 67 filter::filter(data, data.filter.clone()); 66 - return ctx.env.get_undefined(); 68 + return env.get_undefined(); 67 69 } 68 70 69 - #[js_function(0)] 70 - pub fn get_page_track_ids(ctx: CallContext) -> NResult<JsUnknown> { 71 - let data: &mut Data = get_data(&ctx)?; 72 - let page_track_ids = data.get_page_tracks(); 73 - return ctx.env.to_js_value(page_track_ids); 71 + #[napi(js_name = "get_page_track_ids")] 72 + #[allow(dead_code)] 73 + pub fn get_page_track_ids(env: Env) -> Result<Vec<TrackID>> { 74 + let data: &mut Data = get_data(&env)?; 75 + Ok(data.get_page_tracks().to_owned()) 74 76 } 75 77 76 78 fn get_tracklist_track_ids(data: &Data, playlist_id: &str) -> UniResult<Vec<TrackID>> { ··· 107 109 Ok(ids) 108 110 } 109 111 110 - #[js_function(0)] 111 - pub fn get_page_info(ctx: CallContext) -> NResult<JsUnknown> { 112 - let data: &mut Data = get_data(&ctx)?; 112 + #[napi(object)] 113 + pub struct PageInfo { 114 + pub id: String, 115 + #[napi(ts_type = "TrackList")] 116 + pub tracklist: JsUnknown, 117 + pub sort_key: String, 118 + pub sort_desc: bool, 119 + pub length: i64, 120 + } 121 + 122 + #[allow(dead_code)] 123 + #[napi(js_name = "get_page_info")] 124 + pub fn get_page_info(env: Env) -> Result<PageInfo> { 125 + let data = get_data(&env)?; 113 126 let tracklist = data.library.get_tracklist(&data.open_playlist_id)?; 114 127 115 - let v = serde_json::json!({ 116 - "id": data.open_playlist_id, 117 - "tracklist": tracklist, 118 - "sort_key": data.sort_key, 119 - "sort_desc": data.sort_desc, 120 - "length": data.get_page_tracks().len(), 121 - }); 122 - let js = ctx.env.to_js_value(&v)?; 123 - return Ok(js); 128 + Ok(PageInfo { 129 + id: data.open_playlist_id.clone(), 130 + tracklist: env.to_js_value(tracklist)?, 131 + sort_key: data.sort_key.clone(), 132 + sort_desc: data.sort_desc, 133 + length: data.get_page_tracks().len().try_into().expect("Too long"), 134 + }) 135 + // let v = serde_json::json!({ 136 + // "id": data.open_playlist_id, 137 + // "tracklist": tracklist, 138 + // "sort_key": data.sort_key, 139 + // "sort_desc": data.sort_desc, 140 + // "length": data.get_page_tracks().len(), 141 + // }); 142 + // let js = env.to_js_value(&v)?; 143 + // return Ok(js); 124 144 } 125 145 126 - #[js_function(2)] 127 - pub fn sort_js(ctx: CallContext) -> NResult<JsUndefined> { 128 - let data: &mut Data = get_data(&ctx)?; 129 - let sort_key = arg_to_string(&ctx, 0)?; 130 - let keep_filter = arg_to_bool(&ctx, 1)?; 146 + #[napi(js_name = "sort")] 147 + #[allow(dead_code)] 148 + pub fn sort_js(sort_key: String, keep_filter: bool, env: Env) -> Result<JsUndefined> { 149 + let data: &mut Data = get_data(&env)?; 131 150 132 151 let old_sort_key = &data.sort_key; 133 152 if &sort_key == old_sort_key { ··· 146 165 if keep_filter { 147 166 filter::filter(data, data.filter.clone()); 148 167 } 149 - return ctx.env.get_undefined(); 168 + return env.get_undefined(); 169 + } 170 + 171 + #[napi(object)] 172 + pub struct SelectionInfo { 173 + pub from: u32, 174 + pub to: u32, 150 175 } 151 176 152 - #[js_function(2)] 153 - pub fn move_tracks(ctx: CallContext) -> NResult<JsObject> { 154 - let data: &mut Data = get_data(&ctx)?; 155 - let mut indexes_to_move: Vec<u32> = arg_to_number_vector(&ctx, 0)?; 177 + #[napi(js_name = "move_tracks")] 178 + #[allow(dead_code)] 179 + pub fn move_tracks( 180 + mut indexes_to_move: Vec<u32>, 181 + to_index: u32, 182 + env: Env, 183 + ) -> Result<SelectionInfo> { 184 + let data: &mut Data = get_data(&env)?; 156 185 indexes_to_move.sort_unstable(); 157 186 indexes_to_move.dedup(); 158 - let to_index: u32 = arg_to_number(&ctx, 1)?; 159 187 let tracklist = data 160 188 .library 161 189 .trackLists ··· 194 222 start_ids.append(&mut moved_ids); 195 223 start_ids.append(&mut end_ids); 196 224 playlist.tracks = start_ids; 197 - let mut new_selection = ctx.env.create_object()?; 198 - new_selection.set_named_property("from", ctx.env.create_uint32(new_from)?)?; 199 - new_selection.set_named_property("to", ctx.env.create_uint32(new_to)?)?; 200 - return Ok(new_selection); 225 + Ok(SelectionInfo { 226 + from: new_from, 227 + to: new_to, 228 + }) 201 229 }
+52 -57
src-native/playlists.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 - use crate::js::{arg_to_bool, arg_to_number_vector, arg_to_string, arg_to_string_vector}; 4 3 use crate::library_types::{Library, SpecialTrackListName, TrackID, TrackList}; 5 4 use crate::{str_to_option, UniResult}; 6 - use napi::{CallContext, JsUndefined, JsUnknown, Result as NResult}; 7 - use napi_derive::js_function; 5 + use napi::{Env, JsUnknown, Result}; 8 6 use std::collections::HashSet; 9 7 use std::path::PathBuf; 10 8 11 9 #[cfg(target_os = "macos")] 12 10 use trash::macos::TrashContextExtMacos; 13 11 14 - #[js_function(0)] 15 - pub fn get_track_lists(ctx: CallContext) -> NResult<JsUnknown> { 16 - let data: &mut Data = get_data(&ctx)?; 12 + #[napi(js_name = "get_track_lists", ts_return_type = "TrackListsHashMap")] 13 + #[allow(dead_code)] 14 + pub fn get_track_lists(env: Env) -> Result<JsUnknown> { 15 + let data: &mut Data = get_data(&env)?; 17 16 let track_lists = &data.library.trackLists; 18 - let js = ctx.env.to_js_value(&track_lists)?; 17 + let js = env.to_js_value(&track_lists)?; 19 18 return Ok(js); 20 19 } 21 20 22 - #[js_function(2)] 23 - pub fn add_tracks(ctx: CallContext) -> NResult<JsUndefined> { 24 - let data: &mut Data = get_data(&ctx)?; 25 - let playlist_id = arg_to_string(&ctx, 0)?; 26 - let mut track_ids: Vec<String> = arg_to_string_vector(&ctx, 1)?; 21 + #[napi(js_name = "add_tracks_to_playlist")] 22 + #[allow(dead_code)] 23 + pub fn add_tracks(playlist_id: String, mut track_ids: Vec<String>, env: Env) -> Result<()> { 24 + let data: &mut Data = get_data(&env)?; 27 25 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 28 26 TrackList::Playlist(playlist) => playlist, 29 27 TrackList::Folder(_) => throw!("Cannot add track to folder"), 30 28 TrackList::Special(_) => throw!("Cannot add track to special playlist"), 31 29 }; 32 30 playlist.tracks.append(&mut track_ids); 33 - return ctx.env.get_undefined(); 31 + return Ok(()); 34 32 } 35 33 36 - #[js_function(2)] 37 - pub fn filter_duplicates(ctx: CallContext) -> NResult<JsUnknown> { 38 - let data: &mut Data = get_data(&ctx)?; 39 - let playlist_id = arg_to_string(&ctx, 0)?; 40 - let track_ids: Vec<String> = arg_to_string_vector(&ctx, 1)?; 41 - let mut track_ids: HashSet<String> = HashSet::from_iter(track_ids.into_iter()); 34 + #[napi(js_name = "playlist_filter_duplicates")] 35 + #[allow(dead_code)] 36 + pub fn filter_duplicates(playlist_id: String, ids: Vec<String>, env: Env) -> Result<Vec<TrackID>> { 37 + let data: &mut Data = get_data(&env)?; 38 + let mut track_ids: HashSet<String> = HashSet::from_iter(ids.into_iter()); 42 39 let playlist = match data.library.get_tracklist_mut(&playlist_id)? { 43 40 TrackList::Playlist(playlist) => playlist, 44 41 _ => throw!("Cannot check if folder/special contains track"), ··· 48 45 track_ids.remove(track); 49 46 } 50 47 } 51 - ctx.env.to_js_value(&track_ids) 48 + let track_ids: Vec<TrackID> = track_ids.into_iter().collect(); 49 + Ok(track_ids) 52 50 } 53 51 54 - #[js_function(1)] 55 - pub fn remove_from_open(ctx: CallContext) -> NResult<JsUndefined> { 56 - let data: &mut Data = get_data(&ctx)?; 57 - let mut indexes_to_remove: Vec<u32> = arg_to_number_vector(&ctx, 0)?; 52 + #[napi(js_name = "remove_from_open_playlist")] 53 + #[allow(dead_code)] 54 + pub fn remove_from_open(mut indexes_to_remove: Vec<u32>, env: Env) -> Result<()> { 55 + let data: &mut Data = get_data(&env)?; 58 56 indexes_to_remove.sort_unstable(); 59 57 indexes_to_remove.dedup(); 60 58 let playlist = match data.library.get_tracklist_mut(&data.open_playlist_id)? { ··· 80 78 } 81 79 } 82 80 playlist.tracks = new_list; 83 - return ctx.env.get_undefined(); 81 + return Ok(()); 84 82 } 85 83 86 84 fn remove_from_all_playlists(library: &mut Library, id: &str) { ··· 119 117 } 120 118 } 121 119 122 - #[js_function(1)] 123 - pub fn delete_tracks_in_open(ctx: CallContext) -> NResult<JsUndefined> { 124 - let data: &mut Data = get_data(&ctx)?; 125 - let ids_to_delete = { 126 - let mut indexes_to_delete: Vec<u32> = arg_to_number_vector(&ctx, 0)?; 127 - indexes_to_delete.sort_unstable(); 128 - indexes_to_delete.dedup(); 129 - get_page_ids(data, indexes_to_delete)? 130 - }; 120 + #[napi(js_name = "delete_tracks_in_open")] 121 + #[allow(dead_code)] 122 + pub fn delete_tracks_in_open(mut indexes_to_delete: Vec<u32>, env: Env) -> Result<()> { 123 + let data: &mut Data = get_data(&env)?; 124 + indexes_to_delete.sort_unstable(); 125 + indexes_to_delete.dedup(); 126 + let ids_to_delete = get_page_ids(data, indexes_to_delete)?; 131 127 let library = &mut data.library; 132 128 133 129 for id_to_delete in &ids_to_delete { ··· 146 142 .expect("Track ID not found when deleting"); 147 143 delete_file(&file_path)?; 148 144 } 149 - return ctx.env.get_undefined(); 145 + return Ok(()); 150 146 } 151 147 152 - #[js_function(4)] 153 - pub fn new_playlist(ctx: CallContext) -> NResult<JsUndefined> { 154 - let data: &mut Data = get_data(&ctx)?; 148 + #[napi(js_name = "new_playlist")] 149 + #[allow(dead_code)] 150 + pub fn new_playlist( 151 + name: String, 152 + description: String, 153 + is_folder: bool, 154 + parent_id: String, 155 + env: Env, 156 + ) -> Result<()> { 157 + let data: &mut Data = get_data(&env)?; 155 158 let library = &mut data.library; 156 - let name = arg_to_string(&ctx, 0)?; 157 - let description = arg_to_string(&ctx, 1)?; 158 - let is_folder = arg_to_bool(&ctx, 2)?; 159 - let parent_id = arg_to_string(&ctx, 3)?; 160 159 161 160 let list = match is_folder { 162 161 true => { ··· 188 187 }, 189 188 }; 190 189 191 - return ctx.env.get_undefined(); 190 + return Ok(()); 192 191 } 193 192 194 - #[js_function(4)] 195 - pub fn update_playlist(ctx: CallContext) -> NResult<JsUndefined> { 196 - let data: &mut Data = get_data(&ctx)?; 197 - let id = arg_to_string(&ctx, 0)?; 198 - let name = arg_to_string(&ctx, 1)?; 199 - let description = arg_to_string(&ctx, 2)?; 193 + #[napi(js_name = "update_playlist")] 194 + #[allow(dead_code)] 195 + pub fn update_playlist(id: String, name: String, description: String, env: Env) -> Result<()> { 196 + let data: &mut Data = get_data(&env)?; 200 197 201 198 match data.library.trackLists.get_mut(&id) { 202 199 Some(TrackList::Special(_)) => throw!("Cannot edit special playlists"), ··· 211 208 None => throw!("Playlist not found"), 212 209 }; 213 210 214 - return ctx.env.get_undefined(); 211 + return Ok(()); 215 212 } 216 213 217 214 fn get_all_tracklist_children(data: &Data, playlist_id: &str) -> UniResult<Vec<TrackID>> { ··· 251 248 Ok(children) 252 249 } 253 250 254 - #[js_function(3)] 255 - pub fn move_playlist(ctx: CallContext) -> NResult<JsUndefined> { 256 - let data: &mut Data = get_data(&ctx)?; 257 - let id = arg_to_string(&ctx, 0)?; 258 - let from_id = arg_to_string(&ctx, 1)?; 259 - let to_id = arg_to_string(&ctx, 2)?; 251 + #[napi(js_name = "move_playlist")] 252 + #[allow(dead_code)] 253 + pub fn move_playlist(id: String, from_id: String, to_id: String, env: Env) -> Result<()> { 254 + let data: &mut Data = get_data(&env)?; 260 255 261 256 match data.library.trackLists.get(&id) { 262 257 Some(TrackList::Special(_)) => throw!("Cannot move special playlist"), ··· 282 277 let to_folder_children = get_children_if_user_editable(&mut data.library, &to_id)?; 283 278 to_folder_children.push(id.clone()); 284 279 285 - return ctx.env.get_undefined(); 280 + Ok(()) 286 281 }
+12 -15
src-native/sidebar_view.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 - use crate::js::{arg_to_bool, arg_to_string}; 4 3 use crate::library::Paths; 5 4 use crate::{path_to_json, UniResult}; 6 5 use atomicwrites::AtomicFile; 7 6 use atomicwrites::OverwriteBehavior::AllowOverwrite; 8 - use napi::JsUndefined; 9 - use napi::{CallContext, JsUnknown, Result as NResult}; 10 - use napi_derive::js_function; 7 + use napi::{Env, Result}; 11 8 use serde::{Deserialize, Serialize}; 12 9 use std::collections::HashSet; 13 10 use std::io::Write; ··· 40 37 Ok(()) 41 38 } 42 39 } 43 - #[js_function(0)] 44 - pub fn shown_playlist_folders(ctx: CallContext) -> NResult<JsUnknown> { 45 - let data: &Data = get_data(&ctx)?; 46 - let view_cache = &data.view_cache; 47 - ctx.env.to_js_value(&view_cache.shown_playlist_folders) 40 + #[napi(js_name = "shown_playlist_folders")] 41 + #[allow(dead_code)] 42 + pub fn shown_playlist_folders(env: Env) -> Result<Vec<String>> { 43 + let data: &Data = get_data(&env)?; 44 + let shown_folders = &data.view_cache.shown_playlist_folders; 45 + Ok(shown_folders.iter().cloned().collect()) 48 46 } 49 - #[js_function(2)] 50 - pub fn view_folder_set_show(ctx: CallContext) -> NResult<JsUndefined> { 51 - let data: &mut Data = get_data(&ctx)?; 52 - let id: String = arg_to_string(&ctx, 0)?; 53 - let show: bool = arg_to_bool(&ctx, 1)?; 47 + #[napi(js_name = "view_folder_set_show")] 48 + #[allow(dead_code)] 49 + pub fn view_folder_set_show(id: String, show: bool, env: Env) -> Result<()> { 50 + let data: &mut Data = get_data(&env)?; 54 51 if show { 55 52 data.view_cache.shown_playlist_folders.insert(id); 56 53 } else { 57 54 data.view_cache.shown_playlist_folders.remove(&id); 58 55 } 59 56 let _ = data.view_cache.save(&data.paths); 60 - ctx.env.get_undefined() 57 + Ok(()) 61 58 }
+21 -15
src-native/tracks/md.rs
··· 1 + #![allow(non_snake_case)] 2 + 1 3 use super::tag::SetInfoError; 2 4 use super::{generate_filename, Tag}; 3 5 use crate::library_types::Track; ··· 6 8 use std::fs; 7 9 use std::path::PathBuf; 8 10 9 - #[allow(non_snake_case)] 10 11 #[derive(Serialize, Deserialize, Debug)] 12 + #[napi(object)] 11 13 pub struct TrackMD { 12 - name: String, 13 - artist: String, 14 - albumName: String, 15 - albumArtist: String, 16 - composer: String, 17 - grouping: String, 18 - genre: String, 19 - year: String, 20 - trackNum: String, 21 - trackCount: String, 22 - discNum: String, 23 - discCount: String, 24 - bpm: String, 25 - comments: String, 14 + pub name: String, 15 + pub artist: String, 16 + pub albumName: String, 17 + pub albumArtist: String, 18 + pub composer: String, 19 + pub grouping: String, 20 + pub genre: String, 21 + pub year: String, 22 + pub trackNum: String, 23 + pub trackCount: String, 24 + pub discNum: String, 25 + pub discCount: String, 26 + pub bpm: String, 27 + // compilation: String, 28 + // rating: String, 29 + // liked: String, 30 + // playCount: String, 31 + pub comments: String, 26 32 } 27 33 28 34 pub fn update_track_info(
+90 -102
src-native/tracks/mod.rs
··· 1 1 use crate::data::Data; 2 2 use crate::data_js::get_data; 3 3 use crate::get_now_timestamp; 4 - use crate::js::{arg_to_bytes, arg_to_number, arg_to_string, nerr}; 5 - use crate::library_types::Track; 6 - use napi::{ 7 - CallContext, Env, JsArrayBuffer, JsObject, JsUndefined, JsUnknown, Result as NResult, Task, 8 - }; 9 - use napi_derive::js_function; 4 + use crate::js::nerr; 5 + use crate::library_types::{MsSinceUnixEpoch, Track, TrackID}; 6 + use napi::{Env, JsArrayBuffer, JsObject, Result, Task}; 10 7 use std::fs; 11 8 use std::path::{Path, PathBuf}; 12 9 ··· 16 13 17 14 pub use tag::Tag; 18 15 19 - fn id_arg_to_track<'a>(ctx: &'a CallContext, arg: usize) -> NResult<&'a mut Track> { 20 - let data: &mut Data = get_data(&ctx)?; 21 - let id = arg_to_string(&ctx, arg)?; 16 + fn id_to_track<'a>(env: &'a Env, id: &String) -> Result<&'a mut Track> { 17 + let data: &mut Data = get_data(&env)?; 22 18 let tracks = &mut data.library.tracks; 23 - let track = tracks.get_mut(&id).ok_or(nerr("Track ID not found"))?; 19 + let track = tracks.get_mut(id).ok_or(nerr("Track ID not found"))?; 24 20 return Ok(track); 25 21 } 26 22 27 - #[js_function(1)] 28 - pub fn get_track(ctx: CallContext) -> NResult<JsUnknown> { 29 - let data: &mut Data = get_data(&ctx)?; 30 - let id = arg_to_string(&ctx, 0)?; 23 + #[napi(js_name = "get_track")] 24 + #[allow(dead_code)] 25 + pub fn get_track(id: String, env: Env) -> Result<Track> { 26 + let data: &mut Data = get_data(&env)?; 31 27 let tracks = &data.library.tracks; 32 28 let track = tracks.get(&id).ok_or(nerr("Track ID not found"))?; 33 - let js = ctx.env.to_js_value(&track)?; 34 - return Ok(js); 29 + Ok(track.clone()) 35 30 } 36 31 37 - #[js_function(1)] 38 - pub fn add_play(ctx: CallContext) -> NResult<JsUndefined> { 39 - let track = id_arg_to_track(&ctx, 0)?; 32 + #[napi(js_name = "add_play")] 33 + #[allow(dead_code)] 34 + pub fn add_play(track_id: String, env: Env) -> Result<()> { 35 + let track = id_to_track(&env, &track_id)?; 40 36 let timestamp = get_now_timestamp(); 41 37 match &mut track.plays { 42 38 None => track.plays = Some(vec![timestamp]), ··· 46 42 None => track.playCount = Some(1), 47 43 Some(play_count) => *play_count += 1, 48 44 } 49 - return ctx.env.get_undefined(); 45 + Ok(()) 50 46 } 51 47 52 - #[js_function(1)] 53 - pub fn add_skip(ctx: CallContext) -> NResult<JsUndefined> { 54 - let track = id_arg_to_track(&ctx, 0)?; 48 + #[napi(js_name = "add_skip")] 49 + #[allow(dead_code)] 50 + pub fn add_skip(track_id: String, env: Env) -> Result<()> { 51 + let track = id_to_track(&env, &track_id)?; 55 52 let timestamp = get_now_timestamp(); 56 53 match &mut track.skips { 57 54 None => track.skips = Some(vec![timestamp]), ··· 61 58 None => track.skipCount = Some(1), 62 59 Some(skip_count) => *skip_count += 1, 63 60 } 64 - return ctx.env.get_undefined(); 61 + Ok(()) 65 62 } 66 63 67 - #[js_function(3)] 68 - pub fn add_play_time(ctx: CallContext) -> NResult<JsUndefined> { 69 - let data: &mut Data = get_data(&ctx)?; 64 + #[napi(js_name = "add_play_time")] 65 + #[allow(dead_code)] 66 + pub fn add_play_time(id: TrackID, start: MsSinceUnixEpoch, dur_ms: i64, env: Env) -> Result<()> { 67 + let data: &mut Data = get_data(&env)?; 70 68 let tracks = &mut data.library.tracks; 71 - let id = arg_to_string(&ctx, 0)?; 72 69 tracks.get_mut(&id).ok_or(nerr("Track ID not found"))?; 73 - let timestamp: i64 = arg_to_number(&ctx, 1)?; 74 - let duration: i64 = arg_to_number(&ctx, 2)?; 75 - data.library.playTime.push((id, timestamp, duration)); 76 - return ctx.env.get_undefined(); 70 + data.library.playTime.push((id, start, dur_ms)); 71 + Ok(()) 77 72 } 78 73 79 74 struct ReadCover(PathBuf); 80 75 impl Task for ReadCover { 81 76 type Output = Vec<u8>; 82 77 type JsValue = JsArrayBuffer; 83 - fn compute(&mut self) -> NResult<Self::Output> { 78 + fn compute(&mut self) -> Result<Self::Output> { 84 79 let path = &self.0; 85 80 let tag = Tag::read_from_path(path)?; 86 81 match tag.get_image(0) { ··· 91 86 } 92 87 } 93 88 } 94 - fn resolve(&mut self, env: Env, output: Self::Output) -> NResult<Self::JsValue> { 89 + fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> { 95 90 let result = env.create_arraybuffer_with_data(output)?; 96 91 return Ok(result.into_raw()); 97 92 } 98 93 } 99 - #[js_function(1)] 100 - pub fn read_cover_async(ctx: CallContext) -> NResult<JsObject> { 101 - let data: &mut Data = get_data(&ctx)?; 102 - let track = id_arg_to_track(&ctx, 0)?; 94 + #[napi(js_name = "read_cover_async", ts_return_type = "Promise<ArrayBuffer>")] 95 + #[allow(dead_code)] 96 + pub fn read_cover_async(track_id: String, env: Env) -> Result<JsObject> { 97 + let data: &mut Data = get_data(&env)?; 98 + let track = id_to_track(&env, &track_id)?; 103 99 let tracks_dir = &data.paths.tracks_dir; 104 100 let file_path = tracks_dir.join(&track.file); 105 101 let task = ReadCover(file_path); 106 - ctx.env.spawn(task).map(|t| t.promise_object()) 102 + env.spawn(task).map(|t| t.promise_object()) 107 103 } 108 104 109 105 fn sanitize_filename(input: &String) -> String { ··· 144 140 return filename; 145 141 } 146 142 147 - #[js_function(2)] 148 - pub fn import(ctx: CallContext) -> NResult<JsUndefined> { 149 - let data: &mut Data = get_data(&ctx)?; 150 - let track_path_str = arg_to_string(&ctx, 0)?; 151 - let now = arg_to_number(&ctx, 1)?; 152 - let path = Path::new(&track_path_str); 143 + #[napi(js_name = "import_file")] 144 + #[allow(dead_code)] 145 + pub fn import_file(path: String, now: MsSinceUnixEpoch, env: Env) -> Result<()> { 146 + let data: &mut Data = get_data(&env)?; 147 + let path = Path::new(&path); 153 148 let ext = path.extension().unwrap_or_default().to_string_lossy(); 154 149 let track = match ext.as_ref() { 155 150 "mp3" => import::import_mp3(&data, &path, now)?, ··· 159 154 }; 160 155 let id = data.library.generate_id(); 161 156 data.library.tracks.insert(id, track); 162 - return ctx.env.get_undefined(); 157 + Ok(()) 163 158 } 164 159 165 - #[js_function(1)] 166 - pub fn load_tags(ctx: CallContext) -> NResult<JsUndefined> { 167 - let data: &mut Data = get_data(&ctx)?; 160 + #[napi(js_name = "load_tags")] 161 + #[allow(dead_code)] 162 + pub fn load_tags(track_id: String, env: Env) -> Result<()> { 163 + let data: &mut Data = get_data(&env)?; 168 164 data.current_tag = None; 169 - let track = id_arg_to_track(&ctx, 0)?; 165 + let track = id_to_track(&env, &track_id)?; 170 166 171 167 let path = data.paths.tracks_dir.join(&track.file); 172 168 let tag = Tag::read_from_path(&path)?; 173 169 data.current_tag = Some(tag); 174 - ctx.env.get_undefined() 170 + Ok(()) 175 171 } 176 172 177 - pub fn image_to_js_obj(ctx: &CallContext, img: tag::Image) -> NResult<JsObject> { 178 - let mut image_obj = ctx.env.create_object()?; 179 - 180 - let index = ctx.env.create_uint32(img.index as u32)?; 181 - image_obj.set_named_property("index", index)?; 182 - 183 - let total_images = ctx.env.create_uint32(img.total_images as u32)?; 184 - image_obj.set_named_property("total_images", total_images)?; 185 - 186 - let mime_type = ctx.env.create_string(&img.mime_type)?; 187 - image_obj.set_named_property("mime_type", mime_type)?; 188 - 189 - let encoded_data = base64::encode(img.data); 190 - let data_str = ctx.env.create_string(&encoded_data)?; 191 - image_obj.set_named_property("data", data_str)?; 192 - 193 - Ok(image_obj) 173 + #[napi(object)] 174 + pub struct JsImage { 175 + pub index: i64, 176 + pub total_images: i64, 177 + pub mime_type: String, 178 + pub data: String, 194 179 } 195 180 196 - #[js_function(1)] 197 - pub fn get_image(ctx: CallContext) -> NResult<JsUnknown> { 198 - let data: &mut Data = get_data(&ctx)?; 199 - let index: u32 = arg_to_number(&ctx, 0)?; 181 + #[napi(js_name = "get_image")] 182 + #[allow(dead_code)] 183 + pub fn get_image(index: u32, env: Env) -> Result<Option<JsImage>> { 184 + let data: &Data = get_data(&env)?; 200 185 201 186 let tag = match &data.current_tag { 202 187 Some(tag) => tag, 203 - None => return Ok(ctx.env.get_null()?.into_unknown()), 188 + None => return Ok(None), 204 189 }; 205 - let image = match tag.get_image(index as usize) { 190 + let img = match tag.get_image(index as usize) { 206 191 Some(image) => image, 207 - None => return Ok(ctx.env.get_null()?.into_unknown()), 192 + None => return Ok(None), 208 193 }; 209 194 210 - let image_obj = image_to_js_obj(&ctx, image)?; 211 - Ok(image_obj.into_unknown()) 195 + let js_image = JsImage { 196 + index: img.index.try_into().expect("usize conv"), 197 + total_images: img.total_images.try_into().expect("usize conv"), 198 + mime_type: img.mime_type, 199 + data: base64::encode(&img.data), 200 + }; 201 + Ok(Some(js_image)) 212 202 } 213 203 214 - #[js_function(2)] 215 - pub fn set_image(ctx: CallContext) -> NResult<JsUndefined> { 216 - let data: &mut Data = get_data(&ctx)?; 217 - let index: u32 = arg_to_number(&ctx, 0)?; 218 - let path_str = arg_to_string(&ctx, 1)?; 204 + #[napi(js_name = "set_image")] 205 + #[allow(dead_code)] 206 + pub fn set_image(index: u32, path_str: String, env: Env) -> Result<()> { 207 + let data: &mut Data = get_data(&env)?; 219 208 let path = data.paths.tracks_dir.join(path_str); 220 209 match &mut data.current_tag { 221 210 Some(tag) => { ··· 234 223 } 235 224 None => throw!("No tag loaded"), 236 225 }; 237 - ctx.env.get_undefined() 226 + Ok(()) 238 227 } 239 228 240 - #[js_function(3)] 241 - pub fn set_image_data(ctx: CallContext) -> NResult<JsUndefined> { 242 - let data: &mut Data = get_data(&ctx)?; 243 - let index: u32 = arg_to_number(&ctx, 0)?; 244 - let bytes = arg_to_bytes(&ctx, 1)?; 245 - let mime_type = arg_to_string(&ctx, 2)?; 229 + #[napi(js_name = "set_image_data")] 230 + #[allow(dead_code)] 231 + pub fn set_image_data(index: u32, bytes: JsArrayBuffer, mime_type: String, env: Env) -> Result<()> { 232 + let bytes: Vec<u8> = bytes.into_value()?.to_vec(); 233 + let data: &mut Data = get_data(&env)?; 246 234 match &mut data.current_tag { 247 235 Some(tag) => tag.set_image(index as usize, bytes, &mime_type)?, 248 236 None => throw!("No tag loaded"), 249 237 }; 250 - ctx.env.get_undefined() 238 + Ok(()) 251 239 } 252 240 253 - #[js_function(1)] 254 - pub fn remove_image(ctx: CallContext) -> NResult<JsUndefined> { 255 - let data: &mut Data = get_data(&ctx)?; 256 - let index: u32 = arg_to_number(&ctx, 0)?; 241 + #[napi(js_name = "remove_image")] 242 + #[allow(dead_code)] 243 + pub fn remove_image(index: u32, env: Env) -> Result<()> { 244 + let data: &mut Data = get_data(&env)?; 257 245 match data.current_tag { 258 246 Some(ref mut tag) => tag.remove_image(index as usize), 259 247 None => {} 260 248 }; 261 - ctx.env.get_undefined() 249 + Ok(()) 262 250 } 263 251 264 - #[js_function(2)] 265 - pub fn update_track_info(ctx: CallContext) -> NResult<JsUndefined> { 266 - let data: &mut Data = get_data(&ctx)?; 267 - let track = id_arg_to_track(&ctx, 0)?; 268 - let new_info: md::TrackMD = serde_json::from_str(&arg_to_string(&ctx, 1)?)?; 252 + #[napi(js_name = "update_track_info")] 253 + #[allow(dead_code)] 254 + pub fn update_track_info(track_id: String, info: md::TrackMD, env: Env) -> Result<()> { 255 + let data: &mut Data = get_data(&env)?; 256 + let track = id_to_track(&env, &track_id)?; 269 257 270 258 let tag = match &mut data.current_tag { 271 259 Some(tag) => tag, 272 260 None => throw!("No tag loaded"), 273 261 }; 274 - md::update_track_info(&data.paths.tracks_dir, track, tag, new_info)?; 262 + md::update_track_info(&data.paths.tracks_dir, track, tag, info)?; 275 263 276 - ctx.env.get_undefined() 264 + Ok(()) 277 265 }
+5 -1
src/App.svelte
··· 20 20 const { ipcRenderer } = window.require('electron') 21 21 async function itunesImport() { 22 22 const result = await iTunesImport( 23 - paths, 23 + { 24 + library_dir: paths.libraryDir, 25 + library_json: paths.libraryJson, 26 + tracks_dir: paths.tracksDir, 27 + }, 24 28 (status: string) => { 25 29 pageStatus = status 26 30 },
+11 -11
src/components/TrackInfo.svelte
··· 2 2 import { writable } from 'svelte/store' 3 3 import type { Writable } from 'svelte/store' 4 4 import { methods } from '@/lib/data' 5 - import type { Track, Image, TrackID } from '@/lib/libraryTypes' 5 + import type { Track, JsImage, TrackID } from '@/lib/libraryTypes' 6 6 import { visibleModalsCount } from '@/lib/modals' 7 7 8 8 type CurrentList = { ··· 13 13 14 14 const id: Writable<TrackID | null> = writable(null) 15 15 const track: Writable<Track | null> = writable(null) 16 - const image = writable(null as null | Image) 16 + const image = writable(null as null | JsImage) 17 17 18 - function openIndex(index: number) { 18 + async function openIndex(index: number) { 19 19 if (currentList && index >= 0 && index < currentList.ids.length) { 20 20 currentList.index = index 21 21 id.set(currentList.ids[index]) ··· 45 45 if (currentList) openIndex(currentList.index + 1) 46 46 } 47 47 48 - function loadImage(index: number) { 48 + async function loadImage(index: number) { 49 49 image.set(methods.getImage(index)) 50 50 } 51 51 </script> ··· 197 197 } 198 198 } 199 199 function nextImage() { 200 - if ($image && $image.index < $image.total_images - 1) { 200 + if ($image && $image.index < $image.totalImages - 1) { 201 201 loadImage($image.index + 1) 202 202 } 203 203 } ··· 248 248 if (checkShortcut(e, 'Backspace') && $image) { 249 249 methods.removeImage($image.index) 250 250 imageEdited = true 251 - if ($image.index < $image.total_images - 1) { 251 + if ($image.index < $image.totalImages - 1) { 252 252 loadImage($image.index) 253 253 } else { 254 254 loadImage(Math.max(0, $image.index - 1)) ··· 304 304 <svelte:body on:keydown|self={keydownNoneSelected} on:paste={coverPaste} /> 305 305 <Modal showIf={$id !== null} onClose={cancel}> 306 306 <form class="modal" on:submit|preventDefault={() => save()}> 307 - <div class="header" class:has-subtitle={$image !== null && $image.total_images >= 2}> 307 + <div class="header" class:has-subtitle={$image !== null && $image.totalImages >= 2}> 308 308 <div class="cover-area" class:droppable tabindex="0" on:keydown={coverKeydown}> 309 309 <div 310 310 class="cover" ··· 330 330 <img 331 331 class="outline-element" 332 332 alt="" 333 - src={'data:' + $image.mime_type + ';base64,' + $image.data} 333 + src={'data:' + $image.mimeType + ';base64,' + $image.data} 334 334 /> 335 335 {/if} 336 336 </div> 337 - {#if $image !== null && $image.total_images >= 2} 337 + {#if $image !== null && $image.totalImages >= 2} 338 338 {@const imageIndex = $image.index} 339 339 <div class="cover-subtitle"> 340 340 <div class="arrow" class:unclickable={imageIndex <= 0}> ··· 355 355 > 356 356 </div> 357 357 <div class="subtitle-text"> 358 - {$image.index + 1} / {$image.total_images} 358 + {$image.index + 1} / {$image.totalImages} 359 359 </div> 360 - <div class="arrow" class:unclickable={imageIndex >= $image.total_images - 1}> 360 + <div class="arrow" class:unclickable={imageIndex >= $image.totalImages - 1}> 361 361 <svg 362 362 on:click={nextImage} 363 363 clip-rule="evenodd"
+5 -5
src/components/TrackList.svelte
··· 41 41 if (index !== null) { 42 42 let firstIndex = selection.findFirst($selection.list) || 0 43 43 const track = page.getTrack(firstIndex) 44 - ipcRenderer.invoke('revealTrackFile', paths.tracks_dir, track.file) 44 + ipcRenderer.invoke('revealTrackFile', paths.tracksDir, track.file) 45 45 } 46 46 } 47 47 function removeFromPlaylist() { ··· 72 72 }) 73 73 74 74 const sortBy = page.sortBy 75 - $: sortKey = $page.sort_key 75 + $: sortKey = $page.sortKey 76 76 77 77 const selection = newSelection({ 78 78 getItemCount: () => $page.length, ··· 177 177 let dragToIndex: null | number = null 178 178 function onDragOver(e: DragEvent, index: number) { 179 179 if ( 180 - !$page.sort_desc || 181 - $page.sort_key !== 'index' || 180 + !$page.sortDesc || 181 + $page.sortKey !== 'index' || 182 182 $filter || 183 183 $page.tracklist.type !== 'playlist' 184 184 ) { ··· 256 256 <div class="description">{$page.tracklist.description}</div> 257 257 {/if} 258 258 </div> 259 - <div class="row table-header" class:desc={$page.sort_desc}> 259 + <div class="row table-header" class:desc={$page.sortDesc}> 260 260 <div class="c index" class:sort={sortKey === 'index'} on:click={() => sortBy('index')}> 261 261 <span>#</span> 262 262 </div>
+28 -124
src/lib/data.ts
··· 1 1 import { writable } from 'svelte/store' 2 - import type { 3 - MsSinceUnixEpoch, 4 - Track, 5 - Image, 6 - TrackID, 7 - TrackList, 8 - TrackListID, 9 - TrackListsHashMap, 10 - } from './libraryTypes' 11 - import { showMessageBox, addon } from './window' 12 - 13 - type PageInfo = { 14 - id: TrackListID 15 - tracklist: TrackList 16 - sort_key: string 17 - sort_desc: boolean 18 - length: number 19 - } 2 + import type { TrackMd } from './libraryTypes' 3 + import { showMessageBox, addon as innerAddon } from './window' 20 4 21 5 export const isDev = window.isDev 22 6 export const isMac = window.isMac 23 7 export const isWindows = window.isWindows 24 - export type Data = { 25 - get_paths: () => { 26 - library_dir: string 27 - tracks_dir: string 28 - library_json: string 29 - } 30 - save: () => void 31 8 32 - import_track: (path: string, now: MsSinceUnixEpoch) => void 33 - get_track: (id: TrackID) => Track 34 - add_play: (id: TrackID) => void 35 - add_skip: (id: TrackID) => void 36 - add_play_time: (id: TrackID, startTime: MsSinceUnixEpoch, duration_ms: number) => void 37 - read_cover_async: (id: TrackID) => Promise<ArrayBuffer> 38 - update_track_info: (id: TrackID, md: string) => void 39 - load_tags: (id: TrackID) => void 40 - get_image: (index: number) => Image 41 - set_image: (index: number, path: string) => void 42 - set_image_data: (index: number, bytes: ArrayBuffer, mime_type: string) => void 43 - remove_image: (index: number) => void 44 - 45 - get_track_lists: () => TrackListsHashMap 46 - playlist_filter_duplicates: (playlistId: TrackListID, trackIds: TrackID[]) => TrackID[] 47 - add_tracks_to_playlist: (playlistId: TrackListID, trackIds: TrackID[]) => void 48 - remove_from_open_playlist: (indexes: number[]) => void 49 - delete_tracks_in_open: (indexes: number[]) => void 50 - new_playlist: (name: string, description: string, isFolder: boolean, parentId: string) => void 51 - update_playlist: (id: string, name: string, description: string) => void 52 - move_playlist: (id: string, fromParent: string, toParent: string) => void 53 - 54 - refresh_page: () => void 55 - open_playlist: (id: TrackListID) => void 56 - get_page_track_ids: () => TrackID[] 57 - filter_open_playlist: (query: string) => TrackID[] 58 - get_page_track: (index: number) => Track 59 - get_page_track_id: (index: number) => string 60 - get_page_info: () => PageInfo 61 - sort: (key: string, keep_filter: boolean) => void 62 - move_tracks: (indexes: number[], to_index: number) => { from: number; to: number } 63 - 64 - shown_playlist_folders: () => string[] 65 - view_folder_set_show: (id: string, show: boolean) => void 66 - } 9 + call((addon) => addon.load_data(isDev)) 67 10 68 11 function getErrorMessage(err: unknown): string { 69 12 if (typeof err === 'object' && err !== null) { ··· 86 29 return '' 87 30 } 88 31 89 - function runWrapped<T>(cb: () => T): T { 90 - try { 91 - return cb() 92 - } catch (err) { 93 - showMessageBox({ 94 - type: 'error', 95 - message: getErrorMessage(err), 96 - detail: getErrorStack(err), 97 - }) 98 - throw err 99 - } 100 - } 101 - 102 - const dataInternal: Data = runWrapped(() => { 103 - return addon.load_data(isDev) 104 - }) 105 - 106 - const data = dataInternal 107 - export function call<T>(cb: (data: Data) => T): T { 32 + export function call<T>(cb: (addon: typeof innerAddon) => T): T { 108 33 try { 109 - return cb(dataInternal) 34 + return cb(innerAddon) 110 35 } catch (err) { 111 36 showMessageBox({ 112 37 type: 'error', ··· 118 43 } 119 44 120 45 export const trackLists = (() => { 121 - const initial = call((data) => data.get_track_lists()) 46 + const initial = call((addon) => addon.get_track_lists()) 122 47 const { subscribe, set } = writable(initial) 123 48 return { 124 49 subscribe, 125 50 refreshTrackIdList() { 126 - set(call((data) => data.get_track_lists())) 51 + set(call((addon) => addon.get_track_lists())) 127 52 }, 128 53 } 129 54 })() ··· 133 58 checkDuplicates = true 134 59 ) { 135 60 if (checkDuplicates) { 136 - const filteredIds = call((data) => data.playlist_filter_duplicates(playlistId, trackIds)) 61 + const filteredIds = call((addon) => addon.playlist_filter_duplicates(playlistId, trackIds)) 137 62 const duplicates = trackIds.length - filteredIds.length 138 63 139 64 if (duplicates > 0) { ··· 155 80 } 156 81 } 157 82 if (trackIds.length >= 1) { 158 - call((data) => data.add_tracks_to_playlist(playlistId, trackIds)) 83 + call((addon) => addon.add_tracks_to_playlist(playlistId, trackIds)) 159 84 if (page.get().tracklist.id === playlistId) { 160 85 page.refresh() 161 86 } ··· 163 88 } 164 89 } 165 90 export function removeFromOpenPlaylist(indexes: number[]) { 166 - call((data) => data.remove_from_open_playlist(indexes)) 91 + call((addon) => addon.remove_from_open_playlist(indexes)) 167 92 page.refresh() 168 93 methods.save() 169 94 } 170 95 export function deleteTracksInOpen(indexes: number[]) { 171 - call((data) => data.delete_tracks_in_open(indexes)) 96 + call((addon) => addon.delete_tracks_in_open(indexes)) 172 97 page.refresh() 173 98 methods.save() 174 99 } ··· 180 105 id: string 181 106 } 182 107 export function newPlaylist(info: PlaylistInfo) { 183 - call((data) => data.new_playlist(info.name, info.description, info.isFolder, info.id)) 108 + call((addon) => addon.new_playlist(info.name, info.description, info.isFolder, info.id)) 184 109 methods.save() 185 110 trackLists.refreshTrackIdList() 186 111 } 187 112 export function updatePlaylist(id: string, name: string, description: string) { 188 - call((data) => data.update_playlist(id, name, description)) 113 + call((addon) => addon.update_playlist(id, name, description)) 189 114 methods.save() 190 115 trackLists.refreshTrackIdList() 191 116 softRefreshPage.refresh() 192 117 } 193 118 export function movePlaylist(id: TrackListID, fromParent: TrackListID, toParent: TrackListID) { 194 - call((data) => data.move_playlist(id, fromParent, toParent)) 119 + call((addon) => addon.move_playlist(id, fromParent, toParent)) 195 120 methods.save() 196 121 trackLists.refreshTrackIdList() 197 122 } 198 123 199 - export const paths = (() => { 200 - return call((data) => data.get_paths()) 201 - })() 124 + export const paths = call((addon) => addon.get_paths()) 202 125 203 126 export async function importTracks(paths: string[]) { 204 127 let errState = null 205 128 const now = Date.now() 206 129 for (const path of paths) { 207 130 try { 208 - data.import_track(path, now) 131 + innerAddon.import_file(path, now) 209 132 } catch (err) { 210 133 if (errState === 'skip') continue 211 134 const result = await showMessageBox({ ··· 223 146 page.refresh() 224 147 } 225 148 226 - export type TrackMD = { 227 - name: string 228 - artist: string 229 - albumName: string 230 - albumArtist: string 231 - composer: string 232 - grouping: string 233 - genre: string 234 - year: string 235 - trackNum: string 236 - trackCount: string 237 - discNum: string 238 - discCount: string 239 - bpm: string 240 - // compilation: string 241 - // rating: string 242 - // liked: string 243 - // playCount: string 244 - comments: string 245 - } 246 - 247 149 export const methods = { 248 150 importTrack: (path: string, now: MsSinceUnixEpoch) => { 249 - call((data) => data.import_track(path, now)) 151 + call((data) => data.import_file(path, now)) 250 152 }, 251 153 getTrack: (id: TrackID) => { 252 154 return call((data) => data.get_track(id)) 253 155 }, 254 - save: () => data.save(), 156 + save: () => { 157 + return call((addon) => addon.save()) 158 + }, 255 159 addPlay: (id: TrackID) => { 256 160 call((data) => data.add_play(id)) 257 161 methods.save() ··· 267 171 methods.save() 268 172 softRefreshPage.refresh() 269 173 }, 270 - readCoverAsync: (id: TrackID) => data.read_cover_async(id), 271 - updateTrackInfo: (id: TrackID, md: TrackMD) => { 272 - call((data) => data.update_track_info(id, JSON.stringify(md))) 174 + readCoverAsync: (id: TrackID) => innerAddon.read_cover_async(id), 175 + updateTrackInfo: (id: TrackID, md: TrackMd) => { 176 + call((data) => data.update_track_info(id, md)) 273 177 methods.save() 274 178 softRefreshPage.refresh() 275 179 refreshTrackInfo.refresh() ··· 321 225 export const refreshTrackInfo = createRefreshStore() 322 226 export const page = (() => { 323 227 function get() { 324 - const info = call((data) => data.get_page_info()) 228 + const info = call((addon) => addon.get_page_info()) 325 229 return info 326 230 } 327 231 ··· 330 234 subscribe, 331 235 get, 332 236 refresh: () => { 333 - call((data) => data.refresh_page()) 237 + call((addon) => addon.refresh_page()) 334 238 set(get()) 335 239 }, 336 240 setGet: () => { ··· 342 246 filter.set('') 343 247 }, 344 248 sortBy: (key: string) => { 345 - call((data) => data.sort(key, true)) 249 + call((addon) => addon.sort(key, true)) 346 250 set(get()) 347 251 }, 348 252 filter: (query: string) => { 349 253 call((data) => data.filter_open_playlist(query)) 350 254 set(get()) 351 255 }, 352 - getTrack: (index: number): Track => { 353 - return call((data) => data.get_page_track(index)) 256 + getTrack: (index: number) => { 257 + return call((addon) => addon.get_page_track(index)) 354 258 }, 355 259 getTrackId: (index: number) => { 356 260 return call((data) => data.get_page_track_id(index))
+13 -127
src/lib/libraryTypes.ts
··· 1 - export type Library = { 2 - version: Version 3 - playTime: PlayTime[] 4 - tracks: TracksHashMap 5 - trackLists: TrackListsHashMap 6 - } 7 - 8 - export type TrackID = string 9 - export type TrackListID = string 10 - export type MsSinceUnixEpoch = number 11 - /** Should be 0-100 */ 12 - export type PercentInteger = number 13 - export interface TrackListsHashMap { 14 - root: Special 15 - [TrackListID: string]: TrackList 16 - } 17 - 18 - export enum Version { 19 - V1 = 1, 20 - } 21 - 22 - /** (track id, start time, duration) */ 23 - export type PlayTime = [TrackID, number, number] 24 - 25 - export interface TracksHashMap { 26 - [TrackID: string]: Track 27 - } 28 - 29 - export type Track = { 30 - size?: number 31 - duration?: number 32 - bitrate?: number 33 - sampleRate?: number 34 - file: string 35 - dateModified: MsSinceUnixEpoch 36 - dateAdded: MsSinceUnixEpoch 37 - name: string 38 - importedFrom?: string 39 - /** Imported ID, like iTunes Persistent ID */ 40 - originalId?: string 41 - artist: string 42 - composer?: string 43 - sortName?: string 44 - sortArtist?: string 45 - sortComposer?: string 46 - genre?: string 47 - rating?: PercentInteger 48 - year?: number 49 - bpm?: number 50 - comments?: string 51 - grouping?: string 52 - liked?: boolean 53 - disliked?: boolean 54 - disabled?: boolean 55 - compilation?: boolean 56 - albumName?: string 57 - albumArtist?: string 58 - sortAlbumName?: string 59 - sortAlbumArtist?: string 60 - trackNum?: number 61 - trackCount?: number 62 - discNum?: number 63 - discCount?: number 64 - dateImported?: MsSinceUnixEpoch 65 - playCount?: number 66 - plays?: MsSinceUnixEpoch[] 67 - playsImported?: CountObject[] 68 - skipCount?: number 69 - skips?: MsSinceUnixEpoch[] 70 - skipsImported?: CountObject[] 71 - volume?: PercentInteger 72 - } 73 - 74 - export type Image = { 75 - index: number 76 - total_images: number 77 - mime_type: string 78 - data: string 79 - } 80 - 81 - export type CountObject = { 82 - count: number 83 - fromDate: MsSinceUnixEpoch 84 - toDate: MsSinceUnixEpoch 85 - } 86 - 87 - export type TrackList = Playlist | Folder | Special 88 - 89 - export type Playlist = { 90 - type: 'playlist' 91 - id: TrackListID 92 - name: string 93 - description?: string 94 - liked?: string 95 - disliked?: string 96 - importedFrom?: string 97 - originalId?: string 98 - dateImported?: MsSinceUnixEpoch 99 - dateCreated?: MsSinceUnixEpoch 100 - tracks: TrackID[] 101 - } 102 - 103 - export type Folder = { 104 - type: 'folder' 105 - id: TrackListID 106 - name: string 107 - description?: string 108 - liked?: string 109 - disliked?: string 110 - /** For example "itunes" */ 111 - importedFrom?: string 112 - /** For example iTunes Persistent ID */ 113 - originalId?: string 114 - dateImported?: MsSinceUnixEpoch 115 - dateCreated?: MsSinceUnixEpoch 116 - children: TrackListID[] 117 - } 1 + export * from '../../build/addon.d.ts' 2 + export * from '../../build/addon' 118 3 119 - export type Special = { 120 - type: 'special' 121 - id: TrackListID 122 - name: SpecialTrackListName 123 - dateCreated: MsSinceUnixEpoch 124 - children: TrackListID[] 125 - } 126 - 127 - export enum SpecialTrackListName { 128 - Root, 4 + // globals from window.ts 5 + export type { 6 + TrackID, 7 + TrackListID, 8 + PercentInteger, 9 + MsSinceUnixEpoch, 10 + Playlist, 11 + Folder, 12 + Special, 13 + TrackList, 14 + TrackListsHashMap, 129 15 }
+1 -1
src/lib/menus.ts
··· 50 50 }) 51 51 ipcRenderer.on('context.revealTrackFile', (e, id: TrackID) => { 52 52 const track = methods.getTrack(id) 53 - ipcRenderer.invoke('revealTrackFile', paths.tracks_dir, track.file) 53 + ipcRenderer.invoke('revealTrackFile', paths.tracksDir, track.file) 54 54 }) 55 55 ipcRenderer.on('context.Remove from Playlist', (e, indexes: number[]) => { 56 56 removeFromOpenPlaylist(indexes)
+1 -1
src/lib/player.ts
··· 114 114 115 115 function setPlayingFile(id: TrackID, paused = false) { 116 116 const track = methods.getTrack(id) 117 - const fileUrl = 'track:' + joinPaths(paths.tracks_dir, track.file) 117 + const fileUrl = 'track:' + joinPaths(paths.tracksDir, track.file) 118 118 waitingToPlay = !paused 119 119 audio.src = fileUrl 120 120 playingTrack.set(track)
+23 -8
src/lib/window.ts
··· 1 - import type { Data } from './data' 1 + import type * as Addon from '../../build/addon' 2 2 import type { MessageBoxOptions, OpenDialogOptions } from 'electron' 3 3 const electron = window.require('electron') 4 4 ··· 6 6 7 7 const invoke = ipcRenderer.invoke 8 8 9 - type addon = { 10 - copy_file: (from: string, to: string) => void 11 - atomic_file_save: (filePath: string, content: string) => void 12 - load_data: (isDev: boolean) => Data 13 - load_data_async: (isDev: boolean) => Promise<Data> 14 - } 15 9 declare global { 10 + type TrackID = string 11 + type TrackListID = string 12 + type PercentInteger = number 13 + type MsSinceUnixEpoch = number 14 + 15 + interface Playlist extends Addon.Playlist { 16 + type: 'playlist' 17 + } 18 + interface Folder extends Addon.Folder { 19 + type: 'folder' 20 + } 21 + interface Special extends Addon.Special { 22 + type: 'special' 23 + } 24 + type TrackList = Playlist | Folder | Special 25 + 26 + interface TrackListsHashMap { 27 + root: Addon.Special 28 + [TrackListID: string]: TrackList 29 + } 30 + 16 31 interface Window { 17 - addon: addon 32 + addon: typeof Addon 18 33 isDev: boolean 19 34 isMac: boolean 20 35 isWindows: boolean