···
7
7
8
8
const package_json = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')))
9
9
10
10
+
/** @type {import('./src/electron/update').UpdateJson} */
10
11
const update_json = {
11
12
// In the future we can create a stable-1 channel, to avoid notifying users about versions they shouldn't upgrade to. For example if their OS becomes unsupported, or If we decide to drop version migration logic.
12
13
'stable-0': {
13
14
url: package_json.updates_url_prefix + '/v' + package_json.version,
14
14
-
version: 'v' + package_json.version,
15
15
+
version: package_json.version,
15
16
},
16
17
}
17
18
···
7
7
"version": "0.18.0",
8
8
"repository": "https://github.com/probablykasper/ferrum",
9
9
"updates_url_prefix": "https://github.com/probablykasper/ferrum/releases",
10
10
+
"latest-release-api-url": "https://api.github.com/repos/probablykasper/ferrum/releases/latest",
10
11
"scripts": {
11
12
"dev": "npm run napi:dev && vite dev",
12
13
"dev-release": "npm run napi && vite dev",
···
114
114
if !options.sort_desc {
115
115
items.reverse();
116
116
}
117
117
-
println!("Sort: {:.3}ms", now.elapsed().as_secs_f32() * 1000.0);
117
117
+
println!("Sort: {}ms", now.elapsed().as_millis());
118
118
let item_ids = items.into_iter().map(|item| item.item_id).collect();
119
119
return Ok(item_ids);
120
120
}
···
9
9
import PlaylistInfoModal from './components/PlaylistInfo.svelte'
10
10
import { queue_visible } from './lib/queue'
11
11
import { ipc_listen, ipc_renderer } from '@/lib/window'
12
12
-
import { delete_track_list, get_track_list, import_tracks, type PlaylistInfo } from '@/lib/data'
12
12
+
import {
13
13
+
delete_track_list,
14
14
+
get_track_list,
15
15
+
import_tracks,
16
16
+
view_options,
17
17
+
type PlaylistInfo,
18
18
+
} from '@/lib/data'
13
19
import { play_pause } from './lib/player'
14
20
import DragGhost from './components/DragGhost.svelte'
15
21
import ItunesImport from './components/ItunesImport.svelte'
···
18
24
import { check_shortcut } from './lib/helpers'
19
25
import ArtistList from './components/ArtistList.svelte'
20
26
import { tracklist_actions } from './lib/page'
21
21
-
import './lib/router'
22
27
import Route from './lib/Route.svelte'
23
28
import { navigate_back, navigate_forward } from './lib/router'
29
29
+
import './lib/router'
24
30
25
31
ipc_renderer.invoke('app_loaded').catch(() => {
26
32
ipc_renderer.invoke('showMessageBox', false, {
···
29
35
detail: 'Graceful shutdown will not be possible.',
30
36
})
31
37
})
38
38
+
39
39
+
if (window.navigator.onLine) {
40
40
+
ipc_renderer.invoke('check_for_updates')
41
41
+
}
32
42
33
43
async function open_import_dialog() {
34
44
if ($modal_count !== 0) {
···
2
2
import { ipc_main } from './typed_ipc'
3
3
import path from 'path'
4
4
import is from './is'
5
5
+
import { check_for_updates } from './update'
6
6
+
7
7
+
ipc_main.handle('check_for_updates', async (_e) => {
8
8
+
check_for_updates()
9
9
+
})
5
10
6
11
ipc_main.handle('showMessageBox', async (e, attached, options) => {
7
12
const window = BrowserWindow.fromWebContents(e.sender)
···
11
11
import { ipcMain as electronIpcMain } from 'electron'
12
12
import type { TrackID } from '../../ferrum-addon'
13
13
14
14
-
type OptionalPromise<T> = T | Promise<T>
15
14
type InputMap = {
16
15
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17
16
[key: string]: (...args: any[]) => any
···
45
44
listener: (
46
45
event: TypedIpcMainInvokeEvent<IpcEvents>,
47
46
...args: Parameters<IpcCommands[K]>
48
48
-
) => OptionalPromise<ReturnType<IpcCommands[K]>>,
47
47
+
) => ReturnType<IpcCommands[K]>,
49
48
): void
50
49
handleOnce<K extends keyof IpcCommands>(
51
50
channel: K,
52
51
listener: (
53
52
event: TypedIpcMainInvokeEvent<IpcEvents>,
54
53
...args: Parameters<IpcCommands[K]>
55
55
-
) => OptionalPromise<ReturnType<IpcCommands[K]>>,
54
54
+
) => ReturnType<IpcCommands[K]>,
56
55
): void
57
56
removeHandler<K extends keyof IpcCommands>(channel: K): void
58
57
}
···
140
139
queue: boolean
141
140
}
142
141
142
142
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
143
143
+
143
144
type Commands = {
144
145
app_loaded: () => void
145
146
showMessageBox: (
···
150
151
attached: boolean,
151
152
options: Parameters<typeof dialog.showOpenDialog>[0],
152
153
) => ReturnType<typeof dialog.showOpenDialog>
154
154
+
check_for_updates: () => void
153
155
revealTrackFile: (...paths: string[]) => void
154
154
-
show_tracks_menu: (options: ShowTrackMenuOptions) => null | SelectedTracksAction
156
156
+
show_tracks_menu: (options: ShowTrackMenuOptions) => Promise<null | SelectedTracksAction>
155
157
showTracklistMenu: (options: { id: string; isFolder: boolean; isRoot: boolean }) => void
156
156
-
show_columns_menu: (options: { menu: Electron.MenuItemConstructorOptions[] }) => void
158
158
+
show_columns_menu: (options: { menu: MenuItemConstructorOptions[] }) => void
157
159
volume_change: (up: boolean) => void
158
160
159
161
'update:Shuffle': (checked: boolean) => void
···
1
1
+
import { app, dialog, shell } from 'electron'
2
2
+
import package_json from '../../package.json'
3
3
+
4
4
+
export type UpdateJson = {
5
5
+
[channel: string]: {
6
6
+
url: string
7
7
+
version: string
8
8
+
}
9
9
+
}
10
10
+
11
11
+
export type JsonValue =
12
12
+
| string
13
13
+
| number
14
14
+
| boolean
15
15
+
| null
16
16
+
| JsonValue[]
17
17
+
| { [key: string]: JsonValue }
18
18
+
19
19
+
function popup(message: string, detail: string) {
20
20
+
console.error(`${message}: ${detail}`)
21
21
+
dialog.showMessageBox({
22
22
+
type: 'error',
23
23
+
message,
24
24
+
detail,
25
25
+
})
26
26
+
}
27
27
+
28
28
+
async function fetch_json(url: string) {
29
29
+
const response = await fetch(url, {}).catch((error: Error) => error)
30
30
+
31
31
+
if (response instanceof Error) {
32
32
+
return { error: response.message, data: null }
33
33
+
} else if (response.status !== 200) {
34
34
+
return { error: `${response.status}: ${response.statusText}`, data: null }
35
35
+
}
36
36
+
37
37
+
const value: JsonValue = await response.json().catch(() => {
38
38
+
return null
39
39
+
})
40
40
+
if (value === null) {
41
41
+
// also handle JSON null value as error
42
42
+
return { error: 'Could not parse JSON', data: null }
43
43
+
}
44
44
+
return { data: value, error: null }
45
45
+
}
46
46
+
47
47
+
export async function check_for_updates() {
48
48
+
const release_result = await fetch_json(package_json['latest-release-api-url'])
49
49
+
if (release_result.error) {
50
50
+
return popup('Failed to check for updates', release_result.error)
51
51
+
}
52
52
+
const release = release_result.data
53
53
+
54
54
+
if (
55
55
+
!release ||
56
56
+
typeof release !== 'object' ||
57
57
+
!('name' in release) ||
58
58
+
typeof release.name !== 'string'
59
59
+
) {
60
60
+
return popup('Failed to check for updates', 'Could not parse JSON')
61
61
+
}
62
62
+
const update_json_url = `${package_json.repository}/releases/download/${release.name}/update.json`
63
63
+
64
64
+
const update_result = await fetch_json(update_json_url)
65
65
+
if (update_result.error) {
66
66
+
return popup('Failed to check for updates', update_result.error)
67
67
+
}
68
68
+
const update_json = update_result.data as UpdateJson
69
69
+
console.log(update_json)
70
70
+
71
71
+
const channel = update_json['stable-0']
72
72
+
if (!channel) {
73
73
+
return popup('Failed to check for updates', 'Could not find update channel')
74
74
+
}
75
75
+
76
76
+
const app_version = app.getVersion()
77
77
+
78
78
+
if (channel.version === app_version) {
79
79
+
return
80
80
+
}
81
81
+
82
82
+
const result = await dialog.showMessageBox({
83
83
+
type: 'info',
84
84
+
message: 'A new version of Ferrum is available!',
85
85
+
detail: `Ferrum ${channel.version} is available. You are currently on ${app_version}`,
86
86
+
buttons: ['Update', 'Cancel'],
87
87
+
defaultId: 0,
88
88
+
})
89
89
+
if (result.response === 0) {
90
90
+
await shell.openExternal(channel.url)
91
91
+
}
92
92
+
}