[READ-ONLY] Mirror of https://github.com/probablykasper/mr-tagger. Music file tagging app for Mac, Linux and Windows
audio linux macos music tagger tauri windows
0

Configure Feed

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

Ability to close files

Kasper (Aug 26, 2021, 2:39 AM +0200) 8bfff978 89ae4032

+172 -41
+46
src-tauri/Cargo.lock
··· 3 3 version = 3 4 4 5 5 [[package]] 6 + name = "addr2line" 7 + version = "0.16.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 6 15 name = "adler" 7 16 version = "1.0.2" 8 17 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 90 99 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 91 100 92 101 [[package]] 102 + name = "backtrace" 103 + version = "0.3.61" 104 + source = "registry+https://github.com/rust-lang/crates.io-index" 105 + checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" 106 + dependencies = [ 107 + "addr2line", 108 + "cc", 109 + "cfg-if 1.0.0", 110 + "libc", 111 + "miniz_oxide 0.4.4", 112 + "object", 113 + "rustc-demangle", 114 + ] 115 + 116 + [[package]] 93 117 name = "base64" 94 118 version = "0.13.0" 95 119 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 933 957 ] 934 958 935 959 [[package]] 960 + name = "gimli" 961 + version = "0.25.0" 962 + source = "registry+https://github.com/rust-lang/crates.io-index" 963 + checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" 964 + 965 + [[package]] 936 966 name = "gio" 937 967 version = "0.14.3" 938 968 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1460 1490 name = "mr-tagger" 1461 1491 version = "0.1.0" 1462 1492 dependencies = [ 1493 + "backtrace", 1463 1494 "base64", 1464 1495 "id3", 1465 1496 "mp4ameta", ··· 1637 1668 ] 1638 1669 1639 1670 [[package]] 1671 + name = "object" 1672 + version = "0.26.1" 1673 + source = "registry+https://github.com/rust-lang/crates.io-index" 1674 + checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c" 1675 + dependencies = [ 1676 + "memchr", 1677 + ] 1678 + 1679 + [[package]] 1640 1680 name = "once_cell" 1641 1681 version = "1.8.0" 1642 1682 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2179 2219 "web-sys", 2180 2220 "winapi", 2181 2221 ] 2222 + 2223 + [[package]] 2224 + name = "rustc-demangle" 2225 + version = "0.1.21" 2226 + source = "registry+https://github.com/rust-lang/crates.io-index" 2227 + checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 2182 2228 2183 2229 [[package]] 2184 2230 name = "rustc_version"
+1
src-tauri/Cargo.toml
··· 11 11 12 12 [dependencies] 13 13 serde_json = "1.0" 14 + backtrace = "0.3" 14 15 serde = { version = "1.0", features = ["derive"] } 15 16 tauri = { version = "1.0.0-beta.8", features = ["dialog-open", "dialog-save", "shell-open"] } 16 17 id3 = "0.6.4"
+35 -12
src-tauri/src/cmd.rs
··· 3 3 use base64; 4 4 use serde::Serialize; 5 5 use serde_json::Value; 6 + use std::fs; 6 7 use std::path::PathBuf; 7 8 use std::sync::{Arc, Mutex}; 8 9 use std::thread; ··· 12 13 #[derive(Debug, Clone, Serialize)] 13 14 pub struct File { 14 15 path: PathBuf, 16 + dirty: bool, 15 17 #[serde(skip_serializing)] 16 18 metadata: Metadata, 17 19 } ··· 25 27 fn current_file(&mut self) -> Result<&mut File, String> { 26 28 match self.files.get_mut(self.current_index) { 27 29 Some(file) => Ok(file), 28 - None => throw!("Error getting open file"), 30 + None => { 31 + throw!("Error getting open file") 32 + } 29 33 } 30 34 } 31 35 } 32 36 33 37 #[derive(Default)] 34 - pub struct Data(pub Arc<Mutex<App>>); 38 + pub struct AppState(pub Arc<Mutex<App>>); 39 + 40 + type AppArg<'a> = State<'a, AppState>; 35 41 36 42 #[command] 37 43 pub fn error_popup(msg: String, win: tauri::Window) { ··· 41 47 }); 42 48 } 43 49 50 + #[command] 51 + pub fn get_app(app: AppArg<'_>) -> Value { 52 + let app = app.0.lock().unwrap(); 53 + serde_json::to_value(&*app).unwrap() 54 + } 55 + 44 56 fn get_metadata(path: &PathBuf) -> Result<Metadata, String> { 45 57 let ext = path.extension().unwrap_or_default().to_string_lossy(); 46 58 let path_str = path.to_string_lossy(); ··· 71 83 } 72 84 73 85 #[command] 74 - pub async fn open_files(paths: Vec<PathBuf>, app: State<'_, Data>) -> Result<Value, String> { 86 + pub async fn open_files(paths: Vec<PathBuf>, app: AppArg<'_>) -> Result<(), String> { 75 87 let mut app = app.0.lock().unwrap(); 76 88 let initial_len = app.files.len(); 77 89 for path in paths { ··· 80 92 let metadata = get_metadata(&path)?; 81 93 app.files.push(File { 82 94 path: path.clone(), 95 + dirty: false, 83 96 metadata: metadata.clone(), 84 97 }); 85 98 } 86 99 } 87 - if initial_len == 0 { 100 + if initial_len == 0 && app.files.len() >= 1 { 88 101 app.current_index = app.files.len() - 1; 89 102 } 90 - Ok(serde_json::to_value(&*app).unwrap()) 103 + Ok(()) 104 + } 105 + 106 + #[command] 107 + pub async fn close_file(index: usize, app: AppArg<'_>) -> Result<(), String> { 108 + let mut app = app.0.lock().unwrap(); 109 + app.files.remove(index); 110 + if app.current_index >= index && index >= 1 { 111 + app.current_index -= 1; 112 + } 113 + Ok(()) 91 114 } 92 115 93 116 #[command] 94 - pub fn show(index: usize, app: State<'_, Data>) -> Result<Value, String> { 117 + pub fn show(index: usize, app: AppArg<'_>) { 95 118 let mut app = app.0.lock().unwrap(); 96 119 app.current_index = index; 97 - Ok(serde_json::to_value(&*app).unwrap()) 98 120 } 99 121 100 122 #[derive(Serialize)] ··· 104 126 } 105 127 106 128 #[command] 107 - pub fn get_page(app: State<'_, Data>) -> Result<Page, String> { 129 + pub fn get_page(app: AppArg<'_>) -> Option<Page> { 108 130 let mut app = app.0.lock().unwrap(); 109 - let file = app.current_file()?; 110 - Ok(Page { 131 + let file = app.current_file().ok()?; 132 + Some(Page { 111 133 path: file.path.clone(), 112 134 frames: get_frames(&file.metadata), 113 135 }) ··· 122 144 } 123 145 124 146 #[command] 125 - pub fn get_image(index: Option<usize>, app: State<'_, Data>) -> Option<Image> { 147 + pub fn get_image(index: Option<usize>, app: AppArg<'_>) -> Option<Image> { 126 148 let mut app = app.0.lock().unwrap(); 127 149 let file = app.current_file().ok()?; 128 150 let index = match index { ··· 174 196 } 175 197 176 198 #[command] 177 - pub fn remove_image(index: usize, app: State<'_, Data>) -> Result<(), String> { 199 + pub fn remove_image(index: usize, app: AppArg<'_>) -> Result<(), String> { 178 200 let mut app = app.0.lock().unwrap(); 179 201 let file = app.current_file().unwrap(); 202 + file.dirty = true; 180 203 match file.metadata { 181 204 Metadata::Id3(ref mut tag) => { 182 205 let mut pic_frames: Vec<_> = tag
+19 -13
src-tauri/src/main.rs
··· 3 3 windows_subsystem = "windows" 4 4 )] 5 5 6 - use tauri::{api, CustomMenuItem, Menu, MenuItem, Submenu, WindowBuilder, WindowUrl}; 6 + use tauri::api::shell; 7 + use tauri::{CustomMenuItem, Menu, MenuItem, Submenu, WindowBuilder, WindowUrl}; 7 8 8 9 mod cmd; 9 10 mod frames; ··· 38 39 .add_submenu(Submenu::new( 39 40 "File", 40 41 Menu::new() 41 - .add_item(custom_menu("New").accelerator("cmdOrControl+N")) 42 42 .add_item(custom_menu("Open...").accelerator("cmdOrControl+O")) 43 43 .add_native_item(MenuItem::Separator) 44 44 .add_item(custom_menu("Save").disabled().accelerator("cmdOrControl+S")) ··· 80 80 let ctx = tauri::generate_context!(); 81 81 tauri::Builder::default() 82 82 .invoke_handler(tauri::generate_handler![ 83 + cmd::error_popup, 84 + cmd::get_app, 83 85 cmd::open_files, 86 + cmd::close_file, 84 87 cmd::show, 85 88 cmd::get_page, 86 - cmd::error_popup, 87 89 cmd::get_image, 88 - cmd::remove_image 90 + cmd::remove_image, 89 91 ]) 90 92 .create_window("main", WindowUrl::default(), |win, webview| { 91 93 let win = win ··· 100 102 .fullscreen(false); 101 103 return (win, webview); 102 104 }) 103 - .manage(cmd::Data(Default::default())) 105 + .manage(cmd::AppState(Default::default())) 104 106 .menu(menu) 105 - .on_menu_event(|event| match event.menu_item_id() { 106 - "learn-more" => { 107 - api::shell::open( 108 - "https://github.com/probablykasper/mr-tagger".to_string(), 109 - None, 110 - ) 111 - .unwrap(); 107 + .on_menu_event(|event| { 108 + let event_name = event.menu_item_id(); 109 + event.window().emit("menu", event_name).unwrap(); 110 + match event_name { 111 + "Learn More" => { 112 + shell::open( 113 + "https://github.com/probablykasper/mr-tagger".to_string(), 114 + None, 115 + ) 116 + .unwrap(); 117 + } 118 + _ => {} 112 119 } 113 - _ => {} 114 120 }) 115 121 .run(ctx) 116 122 .expect("error while running tauri app");
+65 -16
src/App.svelte
··· 1 1 <script lang="ts"> 2 - import { invoke, dialog } from '@tauri-apps/api' 3 - import { checkShortcut, popup, runCmd } from './scripts/helpers' 2 + import { dialog, event } from '@tauri-apps/api' 3 + import { checkShortcut, runCmd } from './scripts/helpers' 4 4 import PageView from './components/Item.svelte' 5 5 import type { Page } from './components/Item.svelte' 6 6 import FileDrop from './components/FileDrop.svelte' 7 + import { onDestroy } from 'svelte' 7 8 8 9 type File = { 9 10 path: string 11 + dirty: boolean 10 12 } 11 13 type App = { 12 14 current_index: number ··· 16 18 current_index: 0, 17 19 files: [], 18 20 } 19 - ;(async () => { 20 - app = await runCmd<App>('open_files', { paths: [] }) 21 - })() 21 + async function getApp() { 22 + app = await runCmd<App>('get_app') 23 + } 24 + getApp() 22 25 23 26 let page: Page | null = null 24 27 $: if (app) getPage() 25 28 async function getPage() { 26 - let newPage = await runCmd<Page>('get_page') 27 - if (!page || newPage.path !== page.path) { 29 + let newPage = await runCmd<Page | null>('get_page') 30 + if (!page || !newPage || newPage.path !== page.path) { 28 31 page = newPage 29 32 } 30 33 } 31 34 32 35 async function openFiles(paths: string[]) { 33 - app = await runCmd<App>('open_files', { paths }) 36 + await runCmd<App>('open_files', { paths }) 37 + getApp() 34 38 } 35 39 async function openDialog() { 36 40 let paths = await dialog.open({ ··· 47 51 } 48 52 async function show(index: number) { 49 53 if (app.current_index !== index) { 50 - app = await runCmd<App>('show', { index }) 54 + await runCmd<App>('show', { index }) 55 + getApp() 51 56 } 57 + } 58 + async function close(index: number) { 59 + if (app.files[index].dirty) { 60 + let confirmed = await (window.confirm('Close without saving?') as any) 61 + if (!confirmed) return 62 + } 63 + await runCmd('close_file', { index }) 64 + getApp() 52 65 } 53 66 async function filesKeydown(e: KeyboardEvent) { 54 67 if (checkShortcut(e, 'ArrowUp')) { ··· 63 76 } 64 77 } 65 78 } 79 + const unlistenFuture = event.listen('menu', ({ payload }) => { 80 + if (payload === 'Open...') { 81 + openDialog() 82 + } 83 + }) 84 + onDestroy(async () => { 85 + const unlisten = await unlistenFuture 86 + unlisten() 87 + }) 66 88 </script> 67 89 68 90 <main> ··· 72 94 </div> 73 95 <div class="files" tabindex="0" on:keydown={filesKeydown}> 74 96 {#each app.files as file, i} 75 - <div class="row" class:selected={i === app.current_index} on:click={() => show(i)} 76 - >{file.path.replace(/^.*[\\\/]/, '')}</div> 97 + <div class="file" class:selected={i === app.current_index} on:click={() => show(i)}> 98 + <div class="icon x" on:click|stopPropagation={() => close(i)}>x</div> 99 + <div class="icon dirty"> 100 + {#if file.dirty} 101 + <svg width="6" height="6" xmlns="http://www.w3.org/2000/svg"> 102 + <circle cx="3" cy="3" r="2.5" /> 103 + </svg> 104 + {/if} 105 + </div> 106 + {file.path.replace(/^.*[\\\/]/, '')} 107 + </div> 77 108 {/each} 78 109 </div> 79 110 <FileDrop ··· 83 114 </div> 84 115 <div class="main"> 85 116 {#if page} 86 - <PageView item={page} /> 117 + <PageView item={page} on:appRefresh={getApp} /> 87 118 {/if} 88 119 </div> 89 120 </main> ··· 121 152 overflow-y: auto 122 153 height: 100% 123 154 outline: none 124 - .row 155 + .file 156 + display: flex 157 + align-items: center 125 158 padding: 7px 8px 159 + padding-left: 0px 126 160 cursor: default 127 - .row:nth-child(2n) 161 + .icon 162 + display: flex 163 + align-items: center 164 + justify-content: center 165 + width: 6px 166 + margin-left: 6px 167 + margin-right: 4px 168 + .x 169 + display: none 170 + &:hover .x 171 + display: block 172 + &:hover .dirty 173 + display: none 174 + .dirty svg 175 + fill: #ffffff 176 + .file:nth-child(2n) 128 177 background-color: rgba(#ffffff, 0.05) 129 - .row.selected 178 + .file.selected 130 179 background-color: hsl(147, 0%, 35%) 131 - &:focus .row.selected 180 + &:focus .file.selected 132 181 background-color: hsl(147, 70%, 30%) 133 182 </style>
+6
src/components/Item.svelte
··· 16 16 </script> 17 17 18 18 <script lang="ts"> 19 + import { createEventDispatcher } from 'svelte' 19 20 import { runCmd } from '../scripts/helpers' 20 21 21 22 export let item: Page ··· 29 30 async function getImage(index: number | null) { 30 31 image = (await runCmd('get_image', { index })) as Image | null 31 32 } 33 + 34 + const dispatch = createEventDispatcher() 32 35 async function removeArtwork() { 33 36 if (image) { 34 37 await runCmd('remove_image', { index: image.index }) 35 38 getImage(null) 39 + dispatch('appRefresh') 36 40 } 37 41 } 38 42 </script> ··· 84 88 width: 20% 85 89 img 86 90 width: 100% 91 + min-height: 80px 92 + object-fit: contain 87 93 .svg-cover 88 94 width: 100% 89 95 padding-bottom: 100%