[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
1<script lang="ts">
2 import { event } from '@tauri-apps/api'
3 import { checkShortcut, runCmd } from './scripts/helpers'
4 import PageView from './components/Page.svelte'
5 import type { Page } from './components/Page.svelte'
6 import { onDestroy } from 'svelte'
7 import FileDrop from 'svelte-tauri-filedrop'
8 import { fade } from 'svelte/transition'
9 import * as dialog from '@tauri-apps/plugin-dialog'
10
11 type File = {
12 path: string
13 dirty: boolean
14 }
15 type App = {
16 current_index: number
17 files: File[]
18 }
19 let app: App = {
20 current_index: 0,
21 files: [],
22 }
23 async function getApp() {
24 app = await runCmd<App>('get_app')
25 }
26 getApp()
27
28 let page: Page | null = null
29 $: if (app) getPage()
30 async function getPage() {
31 let newPage = await runCmd<Page | null>('get_page')
32 if (!page || !newPage || newPage.path !== page.path) {
33 page = newPage
34 }
35 }
36
37 async function openFiles(paths: string[]) {
38 await runCmd<App>('open_files', { paths })
39 getApp()
40 }
41 let extensions = [
42 //
43 'aiff',
44 'mp3',
45 'm4a',
46 'mp4',
47 'm4p',
48 'm4b',
49 'm4r',
50 'm4v',
51 'opus',
52 'wav',
53 ]
54 async function openDialog() {
55 let paths = await dialog.open({
56 filters: [{ name: 'Audio/Video file', extensions }],
57 multiple: true,
58 directory: false,
59 })
60 if (typeof paths === 'string') {
61 paths = [paths]
62 }
63 if (paths !== null) {
64 await openFiles(paths)
65 }
66 }
67 async function show(index: number) {
68 if (app.current_index !== index) {
69 await runCmd<App>('show', { index })
70 getApp()
71 }
72 }
73 async function close(index: number) {
74 if (app.files[index].dirty) {
75 let confirmed = window.confirm('Close without saving?')
76 if (!confirmed) return
77 }
78 await runCmd('close_file', { index })
79 getApp()
80 }
81 async function saveFile(saveAs: boolean) {
82 await runCmd('save_file', { index: app.current_index, saveAs })
83 getApp()
84 }
85 async function filesKeydown(e: KeyboardEvent) {
86 if (checkShortcut(e, 'ArrowUp')) {
87 e.preventDefault()
88 if (app.current_index >= 1) {
89 show(app.current_index - 1)
90 }
91 } else if (checkShortcut(e, 'ArrowDown')) {
92 e.preventDefault()
93 if (app.current_index < app.files.length - 1) {
94 show(app.current_index + 1)
95 }
96 }
97 }
98 const unlistenFuture = event.listen('menu', async ({ payload }) => {
99 if (payload === 'Open...') {
100 openDialog()
101 } else if (payload === 'Close') {
102 if (app.files.length === 0) {
103 await runCmd('close_window')
104 } else {
105 close(app.current_index)
106 }
107 } else if (payload === 'Save') {
108 saveFile(false)
109 } else if (payload === 'Save As...') {
110 saveFile(true)
111 }
112 })
113 onDestroy(async () => {
114 const unlisten = await unlistenFuture
115 unlisten()
116 })
117</script>
118
119<main>
120 <div class="sidebar">
121 <div class="topbar">
122 <button on:click={openDialog}>Open Files</button>
123 </div>
124 <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
125 <div class="files" tabindex="0" on:keydown={filesKeydown}>
126 {#each app.files as file, i}
127 <!-- svelte-ignore a11y-click-events-have-key-events -->
128 <div class="file" class:selected={i === app.current_index} on:click={() => show(i)}>
129 <div class="icon dirty">
130 {#if file.dirty}
131 <svg width="6" height="6" xmlns="http://www.w3.org/2000/svg">
132 <circle cx="3" cy="3" r="2.5" />
133 </svg>
134 {/if}
135 </div>
136 <!-- svelte-ignore a11y-click-events-have-key-events -->
137 <div class="icon x" on:click|stopPropagation={() => close(i)}>
138 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
139 ><path
140 d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"
141 /></svg
142 >
143 </div>
144 {file.path.replace(/^.*[\\/]/, '')}
145 </div>
146 {/each}
147 </div>
148 <FileDrop {extensions} handleFiles={openFiles} let:files>
149 {#if files.length > 0}
150 <div class="dropzone" transition:fade={{ duration: 100 }} />
151 {/if}
152 </FileDrop>
153 </div>
154 <div class="main">
155 {#if page}
156 <button on:click={() => saveFile(false)} tabindex="0">Save</button>
157 <PageView {page} on:appRefresh={getApp} />
158 {/if}
159 </div>
160</main>
161
162<style lang="sass">
163 :global(body)
164 margin: 0
165 font-family: Arial, Helvetica, sans-serif
166 font-size: 18px
167 background-color: #191B20
168 overflow: hidden
169 user-select: none
170 -webkit-user-select: none
171 main
172 display: flex
173 color: #e6e6e6
174 height: 100vh
175 .main
176 flex-grow: 1
177 width: 0px
178 overflow: auto
179 .sidebar
180 display: flex
181 position: relative
182 flex-direction: column
183 width: 250px
184 height: 100%
185 background-color: #202227
186 border-right: 1px solid rgba(#ffffff, 0.1)
187 font-size: 12px
188 .topbar
189 padding: 8px
190 border-bottom: 1px solid rgba(#ffffff, 0.1)
191 .files
192 overflow-y: auto
193 height: 100%
194 outline: none
195 .file
196 display: flex
197 align-items: center
198 padding: 6px 8px
199 padding-left: 0px
200 cursor: default
201 .icon
202 display: flex
203 align-items: center
204 justify-content: center
205 width: 8px
206 height: 8px
207 flex-shrink: 0
208 padding: 4px
209 margin: 0px 5px
210 border-radius: 2px
211 transition: opacity 180ms ease-out, transform 180ms ease-out
212 &:hover
213 background-color: rgba(#ffffff, 0.15)
214 .icon.x
215 opacity: 0
216 transform: scale(0.5)
217 position: absolute
218 &:hover .icon.x
219 display: flex
220 opacity: 1
221 transform: scale(1)
222 &:hover .icon.dirty
223 opacity: 0
224 transform: scale(0.5)
225 svg
226 fill: #ffffff
227 .file:nth-child(2n)
228 background-color: rgba(#ffffff, 0.05)
229 .file.selected
230 background-color: hsl(147, 0%, 35%)
231 &:focus .file.selected
232 background-color: hsl(147, 70%, 30%)
233 background-color: #103fcb
234 .dropzone
235 position: absolute
236 width: 100%
237 height: 100%
238 top: 0px
239 left: 0px
240 background-color: rgba(#ffffff, 0.2)
241</style>