diff --git a/ferrum-addon/addon.d.ts b/ferrum-addon/addon.d.ts index dcd8ecb..4bc25bb 100644 --- a/ferrum-addon/addon.d.ts +++ b/ferrum-addon/addon.d.ts @@ -23,7 +23,12 @@ export interface CountObject { /** Returns the deleted track lists, including folder children */ export declare function delete_track_list(id: string): void -export declare function delete_tracks_with_item_ids(itemIds: Array): void +export declare function delete_tracks_with_item_ids(itemIds: Array): FerrumStatus + +export type FerrumStatus = + | { type: 'Ok' } + | { type: 'FileDeletionError', field0: string } + | { type: 'SaveError', field0: string } export interface Folder { id: TrackListID @@ -127,13 +132,13 @@ export declare function remove_from_playlist(playlistId: TrackID, itemIds: Array export declare function remove_image(index: number): void -export declare function save(): void +export declare function save(): FerrumStatus export declare function save_view_options(viewOptions: ViewOptions): void export declare function set_image(index: number, pathStr: string): void -export declare function set_image_data(index: number, bytes: Uint8Array): void +export declare function set_image_data(index: number, bytes: ArrayBuffer): void export interface Special { id: TrackListID diff --git a/src-native/data_js.rs b/src-native/data_js.rs index 89afa69..8d9c96e 100644 --- a/src-native/data_js.rs +++ b/src-native/data_js.rs @@ -1,3 +1,4 @@ +use crate::FerrumStatus; use crate::data::Data; use anyhow::Result; use napi::Env; @@ -81,8 +82,10 @@ pub fn get_paths(env: Env) -> PathsJs { #[napi(js_name = "save")] #[allow(dead_code)] -pub fn save(env: Env) -> napi::Result<()> { +pub fn save(env: Env) -> FerrumStatus { let data: &mut Data = get_data(&env); - data.save()?; - Ok(()) + match data.save() { + Ok(_) => FerrumStatus::Ok, + Err(err) => FerrumStatus::SaveError(err.to_string()), + } } diff --git a/src-native/lib.rs b/src-native/lib.rs index eaf5788..822dbf9 100644 --- a/src-native/lib.rs +++ b/src-native/lib.rs @@ -20,6 +20,21 @@ mod sort; mod tracks; mod view_options; +#[napi] +pub enum FerrumStatus { + Ok, + FileDeletionError(String), + SaveError(String), +} +impl FerrumStatus { + pub fn is_err(&self) -> bool { + match self { + FerrumStatus::Ok => false, + _ => true, + } + } +} + fn get_now_timestamp() -> i64 { let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(n) => n.as_millis() as i64, diff --git a/src-native/library.rs b/src-native/library.rs index 66ca806..9025753 100644 --- a/src-native/library.rs +++ b/src-native/library.rs @@ -149,16 +149,16 @@ pub fn get_track_field_type(field: &str) -> Result { #[napi(js_name = "get_genres")] #[allow(dead_code)] -pub fn get_genres(env: Env) -> Result> { +pub fn get_genres(env: Env) -> Vec { let data: &mut Data = get_data(&env); let genres = data.library.get_genres(); - Ok(genres.clone()) + genres.clone() } #[napi(js_name = "get_artists")] #[allow(dead_code)] -pub fn get_artists(env: Env) -> Result> { +pub fn get_artists(env: Env) -> Vec { let data: &mut Data = get_data(&env); let genres = data.library.get_artists(); - Ok(genres.clone()) + genres.clone() } diff --git a/src-native/library_types.rs b/src-native/library_types.rs index 29c2277..4063f5f 100644 --- a/src-native/library_types.rs +++ b/src-native/library_types.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] -use crate::get_now_timestamp; use crate::playlists::{delete_file, remove_from_all_playlists}; +use crate::{FerrumStatus, get_now_timestamp}; use anyhow::{Context, Result, bail}; use linked_hash_map::{Entry, LinkedHashMap}; use nanoid::nanoid; @@ -138,13 +138,20 @@ impl Library { track_id_map.push(id.clone()); self.track_item_ids.insert(id, item_id); } - pub fn delete_track_and_file(&mut self, id: &TrackID, tracks_dir: &PathBuf) -> Result<()> { + pub fn delete_track_and_file( + &mut self, + id: &TrackID, + tracks_dir: &PathBuf, + ) -> Result { let file_path = { let track = self.get_track(id)?; tracks_dir.join(&track.file) }; if !file_path.exists() { - bail!("File does not exist: {}", file_path.to_string_lossy()); + return Ok(FerrumStatus::FileDeletionError(format!( + "File does not exist: {}", + file_path.to_string_lossy() + ))); } remove_from_all_playlists(self, id); @@ -154,8 +161,10 @@ impl Library { self.track_item_ids .remove(id) .expect("Track ID not found when deleting (2)"); - delete_file(&file_path)?; - Ok(()) + Ok(match delete_file(&file_path) { + Ok(_) => FerrumStatus::Ok, + Err(err) => FerrumStatus::FileDeletionError(err.to_string()), + }) } pub fn generate_id(&self) -> String { let alphabet: [char; 32] = [ diff --git a/src-native/playlists.rs b/src-native/playlists.rs index 583d718..e883b3a 100644 --- a/src-native/playlists.rs +++ b/src-native/playlists.rs @@ -4,7 +4,7 @@ use crate::library_types::{ ItemId, Library, SpecialTrackListName, TRACK_ID_MAP, TrackID, TrackList, get_track_ids_from_item_ids, new_item_ids_from_track_ids, }; -use crate::str_to_option; +use crate::{FerrumStatus, str_to_option}; use anyhow::{Context, Result, bail}; use linked_hash_map::LinkedHashMap; use napi::{Env, Unknown}; @@ -25,10 +25,9 @@ pub struct TrackListDetails { #[napi(js_name = "get_track_lists_details")] #[allow(dead_code)] -pub fn get_track_lists_details(env: Env) -> Result> { +pub fn get_track_lists_details(env: Env) -> HashMap { let data: &Data = get_data(&env); - Ok(data - .library + data.library .trackLists .iter() .map(|(id, tracklist)| { @@ -54,7 +53,7 @@ pub fn get_track_lists_details(env: Env) -> Result Result<()> { #[napi(js_name = "delete_tracks_with_item_ids")] #[allow(dead_code)] -pub fn delete_tracks_with_item_ids(item_ids: Vec, env: Env) -> Result<()> { +pub fn delete_tracks_with_item_ids(item_ids: Vec, env: Env) -> Result { let data: &mut Data = get_data(&env); let library = &mut data.library; let track_ids = get_track_ids_from_item_ids(&item_ids); for track_id in &track_ids { - library.delete_track_and_file(track_id, &data.paths.tracks_dir)?; + let status = library.delete_track_and_file(track_id, &data.paths.tracks_dir)?; + if status.is_err() { + return Ok(status); + } } - return Ok(()); + return Ok(FerrumStatus::Ok); } #[napi(js_name = "new_playlist")] diff --git a/src-native/tracks/mod.rs b/src-native/tracks/mod.rs index 04cb1de..c29faed 100644 --- a/src-native/tracks/mod.rs +++ b/src-native/tracks/mod.rs @@ -50,21 +50,21 @@ pub fn get_track_by_item_id(item_id: ItemId, env: Env) -> Result { #[napi(js_name = "get_track_ids")] #[allow(dead_code)] -pub fn get_track_ids(item_ids: Vec) -> Result> { +pub fn get_track_ids(item_ids: Vec) -> Vec { let id_map = TRACK_ID_MAP.read().unwrap(); let track_ids = item_ids.into_iter().map(|item_id| { let track_id = &id_map[item_id as usize]; track_id.clone() }); - Ok(track_ids.collect()) + track_ids.collect() } #[napi(js_name = "track_exists")] #[allow(dead_code)] -pub fn track_exists(id: String, env: Env) -> Result { +pub fn track_exists(id: String, env: Env) -> bool { let data: &mut Data = get_data(&env); let tracks = &data.library.get_tracks(); - Ok(tracks.contains_key(&id)) + tracks.contains_key(&id) } #[napi(js_name = "add_play")] @@ -218,13 +218,12 @@ pub fn set_image(index: u32, path_str: String, env: Env) -> Result<()> { #[napi(js_name = "set_image_data")] #[allow(dead_code)] pub fn set_image_data(index: u32, bytes: ArrayBuffer, env: Env) -> Result<()> { - let bytes: Vec = bytes.to_vec(); let data: &mut Data = get_data(&env); let tag = match &mut data.current_tag { Some(tag) => tag, None => bail!("No tag loaded"), }; - tag.set_image(index as usize, bytes)?; + tag.set_image(index as usize, bytes.to_vec())?; Ok(()) } diff --git a/src/lib/data.ts b/src/lib/data.ts index 7dbe8da..7658894 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -3,7 +3,6 @@ import { ipc_renderer } from '@/lib/window' import type { MsSinceUnixEpoch, TrackID, - TrackList, TrackListID, ItemId, TrackMd, @@ -13,7 +12,7 @@ import type { import { queue } from './queue' import { current_playlist_id } from '@/components/TrackList.svelte' import { navigate } from './router' -import { call, get_error_message, strict_call } from './error' +import { call, error_popup, get_error_message, strict_call } from './error' export const is_dev = window.is_dev export const local_data_path = window.local_data_path @@ -23,21 +22,21 @@ export const is_windws = window.is_windows const inner_addon = window.addon export const ItunesImport = inner_addon.ItunesImport -call((addon) => addon.load_data(is_dev, local_data_path, library_path)) +strict_call((addon) => addon.load_data(is_dev, local_data_path, library_path)) -export const paths = call((addon) => addon.get_paths()) +export const paths = strict_call((addon) => addon.get_paths()) export function join_paths(...args: string[]) { return args.join(paths.pathSeparator) } export const track_lists_details_map = (() => { - const initial = call((addon) => addon.get_track_lists_details()) + const initial = strict_call((addon) => addon.get_track_lists_details()) const { subscribe, set } = writable(initial) return { subscribe, refresh() { - set(call((addon) => addon.get_track_lists_details())) + set(strict_call((addon) => addon.get_track_lists_details())) }, } })() @@ -47,7 +46,9 @@ export async function add_tracks_to_playlist( check_duplicates = true, ) { if (check_duplicates) { - const filtered_ids = call((addon) => addon.playlist_filter_duplicates(playlist_id, track_ids)) + const filtered_ids = strict_call((addon) => + addon.playlist_filter_duplicates(playlist_id, track_ids), + ) const duplicates = track_ids.length - filtered_ids.length if (duplicates > 0) { const result = await ipc_renderer.invoke('showMessageBox', false, { @@ -68,18 +69,22 @@ export async function add_tracks_to_playlist( } } if (track_ids.length >= 1) { - call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) + strict_call((addon) => addon.add_tracks_to_playlist(playlist_id, track_ids)) tracklist_updated.emit() save() } } export function remove_from_playlist(playlist_id: TrackListID, item_ids: ItemId[]) { - call((addon) => addon.remove_from_playlist(playlist_id, item_ids)) + strict_call((addon) => addon.remove_from_playlist(playlist_id, item_ids)) tracklist_updated.emit() save() } export function delete_tracks_with_item_ids(item_ids: ItemId[]) { - call((addon) => addon.delete_tracks_with_item_ids(item_ids)) + const result = strict_call((addon) => addon.delete_tracks_with_item_ids(item_ids)) + if (result.type === 'FileDeletionError') { + // Gracefully handle + error_popup(result.field0) + } tracklist_updated.emit() queue.removeDeleted() save() @@ -93,12 +98,12 @@ export type PlaylistInfo = { editMode: boolean } export function new_playlist(info: PlaylistInfo) { - call((addon) => addon.new_playlist(info.name, info.description, info.isFolder, info.id)) + strict_call((addon) => addon.new_playlist(info.name, info.description, info.isFolder, info.id)) track_lists_details_map.refresh() save() } export function update_playlist(id: string, name: string, description: string) { - call((addon) => addon.update_playlist(id, name, description)) + strict_call((addon) => addon.update_playlist(id, name, description)) track_lists_details_map.refresh() tracklist_updated.emit() save() @@ -109,7 +114,7 @@ export function move_playlist( to_parent: TrackListID, to_index: number, ) { - call((addon) => addon.move_playlist(id, from_parent, to_parent, to_index)) + strict_call((addon) => addon.move_playlist(id, from_parent, to_parent, to_index)) track_lists_details_map.refresh() save() } @@ -138,32 +143,32 @@ export async function import_tracks(paths: string[]) { } export function get_default_sort_desc(field: string) { - return call((data) => data.get_default_sort_desc(field)) + return strict_call((data) => data.get_default_sort_desc(field)) } export function import_track(path: string, now: MsSinceUnixEpoch) { call((data) => data.import_file(path, now)) } export function get_track(id: TrackID) { - return call((data) => data.get_track(id)) + return strict_call((data) => data.get_track(id)) } export function get_track_by_item_id(item_id: ItemId) { - return call((data) => data.get_track_by_item_id(item_id)) + return strict_call((data) => data.get_track_by_item_id(item_id)) } export function get_track_ids(item_ids: ItemId[]) { - return call((data) => data.get_track_ids(item_ids)) + return strict_call((data) => data.get_track_ids(item_ids)) } export function get_tracks_page(options: TracksPageOptions) { - return call((data) => data.get_tracks_page(options)) + return strict_call((data) => data.get_tracks_page(options)) } export function track_exists(id: TrackID) { - return call((data) => data.track_exists(id)) + return strict_call((data) => data.track_exists(id)) } export function get_track_list(id: TrackListID) { - return call((data) => data.get_track_list(id)) as TrackList + return strict_call((data) => data.get_track_list(id)) } export function delete_track_list(id: TrackListID) { - call((data) => data.delete_track_list(id)) + strict_call((data) => data.delete_track_list(id)) if (id === get(current_playlist_id)) { navigate('/playlist/root') } @@ -171,7 +176,11 @@ export function delete_track_list(id: TrackListID) { save() } export function save() { - return call((addon) => addon.save()) + const result = strict_call((addon) => addon.save()) + if (result.type === 'SaveError') { + // Gracefully handle + error_popup(result.field0) + } } export function add_play(id: TrackID) { call((data) => data.add_play(id)) diff --git a/src/lib/error.ts b/src/lib/error.ts index 8580558..a46904a 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -8,6 +8,8 @@ export function get_error_message(err: unknown): string { } else if (obj.code) { return 'Code: ' + String(obj.message) } + } else if (typeof err === 'string') { + return err } return 'No reason or code provided' } @@ -20,7 +22,7 @@ function get_error_stack(err: unknown): string { } return '' } -function error_popup(err: unknown, crash = false) { +export function error_popup(err: unknown, crash = false) { ipc_renderer.invoke( 'showMessageBox', false, diff --git a/src/lib/selection.ts b/src/lib/selection.ts index c8999a3..d9e27cc 100644 --- a/src/lib/selection.ts +++ b/src/lib/selection.ts @@ -196,11 +196,9 @@ class Selection { } else if (this.items.size === 0) { this.add_index_unchecked(0) } else if (this.last_added !== null) { - console.log('go_forward', { ...this }) const next_index = this.last_added.index + 1 this.clear() this.add_index_unchecked(Math.min(next_index, this.all.length - 1)) - console.log('.', { ...this }) } } /** Expand or shrink selection backwards (shift+up) */