@@ -263,8 +272,10 @@ import { useSettingsStore, type CloseAction } from './stores/settings';
import PlayerBar from './components/PlayerBar.vue';
import ToastContainer from './components/ToastContainer.vue';
import CommentSection from './components/CommentSection.vue';
+import UpdateDialog from './components/UpdateDialog.vue';
import { usePlayerStore } from './stores/player';
import { useLyric } from './composables/UserLyric';
+import { useUpdater } from './composables/useUpdater';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { listen } from '@tauri-apps/api/event';
import { register, unregister } from '@tauri-apps/plugin-global-shortcut';
@@ -274,6 +285,7 @@ const route = useRoute();
const userStore = useUserStore();
const player = usePlayerStore();
const settings = useSettingsStore();
+const updater = useUpdater();
const createdPlaylists = ref
([]);
const subPlaylists = ref([]);
@@ -426,6 +438,8 @@ onMounted(async () => {
});
}
} catch {}
+
+ updater.checkForUpdate(true);
});
const currentWindow = getCurrentWindow();
diff --git a/src/components/UpdateDialog.vue b/src/components/UpdateDialog.vue
new file mode 100644
index 0000000..adc8587
--- /dev/null
+++ b/src/components/UpdateDialog.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
发现新版本
+
+ v{{ info.currentVersion }} → v{{ info.version }}
+
+
+
+
{{ formatDate(info.date) }}
+
+
+
+
+
+
+
+
正在下载更新 {{ downloadProgress }}%
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useUpdater.ts b/src/composables/useUpdater.ts
new file mode 100644
index 0000000..a6a2cb0
--- /dev/null
+++ b/src/composables/useUpdater.ts
@@ -0,0 +1,143 @@
+import { ref } from 'vue'
+import { check } from '@tauri-apps/plugin-updater'
+import { relaunch } from '@tauri-apps/plugin-process'
+import { getVersion } from '@tauri-apps/api/app'
+
+export interface UpdateInfo {
+ version: string
+ date: string | null
+ body: string | null
+}
+
+const IGNORED_VERSION_KEY = 'updater_ignored_version'
+
+export function useUpdater() {
+ const checking = ref(false)
+ const downloading = ref(false)
+ const downloadProgress = ref(0)
+ const updateAvailable = ref(false)
+ const updateInfo = ref(null)
+ const currentVersion = ref('')
+ const error = ref('')
+
+ async function getCurrentVersion() {
+ try {
+ currentVersion.value = await getVersion()
+ } catch {
+ currentVersion.value = ''
+ }
+ }
+
+ function getIgnoredVersion(): string {
+ try {
+ return localStorage.getItem(IGNORED_VERSION_KEY) || ''
+ } catch {
+ return ''
+ }
+ }
+
+ function setIgnoredVersion(version: string) {
+ try {
+ localStorage.setItem(IGNORED_VERSION_KEY, version)
+ } catch {}
+ }
+
+ async function checkForUpdate(silent = false): Promise {
+ if (checking.value) return null
+ checking.value = true
+ error.value = ''
+ updateAvailable.value = false
+ updateInfo.value = null
+
+ try {
+ await getCurrentVersion()
+ const result = await check()
+
+ if (!result) {
+ if (!silent) error.value = '当前已是最新版本'
+ return null
+ }
+
+ const info: UpdateInfo = {
+ version: result.version,
+ date: result.date ?? null,
+ body: result.body ?? null,
+ }
+
+ const ignored = getIgnoredVersion()
+ if (info.version === ignored) {
+ if (!silent) error.value = '当前已是最新版本'
+ return null
+ }
+
+ updateAvailable.value = true
+ updateInfo.value = info
+ return info
+ } catch (e: any) {
+ if (!silent) error.value = `检查更新失败: ${e}`
+ return null
+ } finally {
+ checking.value = false
+ }
+ }
+
+ async function downloadAndInstall() {
+ if (downloading.value) return
+ downloading.value = true
+ downloadProgress.value = 0
+ error.value = ''
+
+ try {
+ const result = await check()
+ if (!result) {
+ error.value = '未找到可用更新'
+ return
+ }
+
+ let downloaded = 0
+ let contentLength = 0
+ await result.downloadAndInstall((event) => {
+ switch (event.event) {
+ case 'Started':
+ contentLength = event.data.contentLength ?? 0
+ break
+ case 'Progress':
+ downloaded += event.data.chunkLength
+ if (contentLength > 0) {
+ downloadProgress.value = Math.round((downloaded / contentLength) * 100)
+ }
+ break
+ case 'Finished':
+ downloadProgress.value = 100
+ break
+ }
+ })
+
+ await relaunch()
+ } catch (e: any) {
+ error.value = `更新失败: ${e}`
+ } finally {
+ downloading.value = false
+ }
+ }
+
+ function ignoreVersion(version: string) {
+ setIgnoredVersion(version)
+ updateAvailable.value = false
+ updateInfo.value = null
+ }
+
+ return {
+ checking,
+ downloading,
+ downloadProgress,
+ updateAvailable,
+ updateInfo,
+ currentVersion,
+ error,
+ checkForUpdate,
+ downloadAndInstall,
+ ignoreVersion,
+ getCurrentVersion,
+ }
+}
diff --git a/src/views/Settings.vue b/src/views/Settings.vue
index f10976e..aab862e 100644
--- a/src/views/Settings.vue
+++ b/src/views/Settings.vue
@@ -146,7 +146,7 @@
关于
-
-
-
-
-
-
最新版本日志
- v{{ latestRelease.tag_name?.replace('v', '') }}
-
-
{{ formatDate(latestRelease.published_at) }}
-
-
-
{{ latestRelease.body }}
-
-
-
-
-
-
-
-
-
@@ -224,6 +195,7 @@
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useSettingsStore, qualityLabels, closeActionLabels, defaultShortcuts, type CloseAction } from '../stores/settings';
import { useToast } from '../composables/useToast';
+import { useUpdater } from '../composables/useUpdater';
import { invoke } from '@tauri-apps/api/core';
import { getVersion } from '@tauri-apps/api/app';
import { openUrl } from '@tauri-apps/plugin-opener';
@@ -232,6 +204,7 @@ import CustomSelect from '../components/CustomSelect.vue';
const settings = useSettingsStore();
const { showToast } = useToast();
+const updater = useUpdater();
const appVersion = ref('');
const defaultDownloadPath = ref('');
@@ -264,45 +237,15 @@ function clearDownloadPath() {
showToast('已重置为默认路径', 'success');
}
-const checkingUpdate = ref(false);
-const updateMessage = ref('');
-const updateMessageClass = ref('text-content-2');
-const latestRelease = ref
(null);
-const showUpdateModal = ref(false);
-
const themeOptions = [
{ label: '深色', value: 'dark' as const },
{ label: '浅色', value: 'light' as const },
];
-async function checkUpdate() {
- checkingUpdate.value = true;
- updateMessage.value = '';
- try {
- const resp = await fetch('https://gitea.atdunbg.xyz/api/v1/repos/atdunbg/Nekosonic-Music/releases?limit=1&draft=false');
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const releases = await resp.json();
- if (releases && releases.length > 0) {
- latestRelease.value = releases[0];
- showUpdateModal.value = true;
- } else {
- updateMessage.value = '暂无发布版本';
- updateMessageClass.value = 'text-content-3';
- }
- } catch (e: any) {
- updateMessage.value = `获取失败: ${e}`;
- updateMessageClass.value = 'text-danger';
- } finally {
- checkingUpdate.value = false;
- }
-}
-
-function formatDate(dateStr: string) {
- try {
- const d = new Date(dateStr);
- return d.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
- } catch {
- return dateStr;
+async function handleCheckUpdate() {
+ const result = await updater.checkForUpdate(false);
+ if (!result) {
+ showToast(updater.error.value || '当前已是最新版本', 'info');
}
}