diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
index 8bdb11e..6957ee0 100644
--- a/build/entitlements.mac.plist
+++ b/build/entitlements.mac.plist
@@ -16,5 +16,7 @@
com.apple.security.files.downloads.read-write
+ com.apple.security.device.microphone
+
diff --git a/electron-builder.yml b/electron-builder.yml
index 7066623..7533866 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -24,6 +24,7 @@ mac:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
+ - NSMicrophoneUsageDescription: Application requests access to the microphone for audio visualization.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
diff --git a/src/i18n/lang/en-US/comp.ts b/src/i18n/lang/en-US/comp.ts
index 04ae1d9..46cad7b 100644
--- a/src/i18n/lang/en-US/comp.ts
+++ b/src/i18n/lang/en-US/comp.ts
@@ -52,6 +52,31 @@ export default {
copyFailed: 'Copy failed',
backgroundDownload: 'Background Download'
},
+ disclaimer: {
+ title: 'Terms of Use',
+ warning:
+ 'This application is a development test version. Functions are not yet perfect, and there may be many problems and bugs. It is for learning and exchange only.',
+ item1:
+ 'This application is for personal learning, research and technical exchange only. Please do not use it for any commercial purposes.',
+ item2:
+ 'Please delete it within 24 hours after downloading. If you need to use it for a long time, please support the genuine music service.',
+ item3:
+ 'By using this application, you understand and assume the relevant risks. The developer is not responsible for any loss.',
+ agree: 'I have read and agree',
+ disagree: 'Disagree and Exit'
+ },
+ donate: {
+ title: 'Support Developer',
+ subtitle: 'Your support is my motivation',
+ tip: 'Donation is completely voluntary. All functions can be used normally without donation. Thank you for your understanding and support!',
+ wechat: 'WeChat',
+ alipay: 'Alipay',
+ wechatQR: 'WeChat QR Code',
+ alipayQR: 'Alipay QR Code',
+ scanTip: 'Please use your phone to scan the QR code above to donate',
+ enterApp: 'Enter App',
+ noForce: 'No forced donation, click to enter'
+ },
coffee: {
title: 'Buy me a coffee',
alipay: 'Alipay',
diff --git a/src/i18n/lang/ja-JP/comp.ts b/src/i18n/lang/ja-JP/comp.ts
index 2920f79..ba61f3c 100644
--- a/src/i18n/lang/ja-JP/comp.ts
+++ b/src/i18n/lang/ja-JP/comp.ts
@@ -52,6 +52,31 @@ export default {
copyFailed: 'コピーに失敗しました',
backgroundDownload: 'バックグラウンドダウンロード'
},
+ disclaimer: {
+ title: '使用上の注意',
+ warning:
+ 'このアプリは開発テスト版であり、機能が不完全で、多くの問題やバグが存在する可能性があります。学習と交流のみを目的としています。',
+ item1:
+ 'このアプリは個人の学習、研究、技術交流のみを目的としています。商業目的で使用しないでください。',
+ item2:
+ 'ダウンロード後24時間以内に削除してください。長期使用を希望される場合は、正規の音楽サービスをサポートしてください。',
+ item3:
+ 'このアプリを使用することで、関連するリスクを理解し、負担するものとします。開発者は一切の損失に対して責任を負いません。',
+ agree: '以上の内容を読み、同意します',
+ disagree: '同意せずに終了'
+ },
+ donate: {
+ title: '開発者を支援',
+ subtitle: '皆様のサポートが私の原動力です',
+ tip: '寄付は完全に任意です。寄付しなくてもすべての機能を通常通り使用できます。ご理解とご支援に感謝します!',
+ wechat: 'WeChat',
+ alipay: 'Alipay',
+ wechatQR: 'WeChat 受取コード',
+ alipayQR: 'Alipay 受取コード',
+ scanTip: 'スマートフォンのアプリで上記のQRコードをスキャンして寄付してください',
+ enterApp: 'アプリに入る',
+ noForce: '寄付は強制ではありません。クリックして入れます'
+ },
coffee: {
title: 'コーヒーをおごる',
alipay: 'Alipay',
diff --git a/src/i18n/lang/ko-KR/comp.ts b/src/i18n/lang/ko-KR/comp.ts
index b25a4e4..bbef92c 100644
--- a/src/i18n/lang/ko-KR/comp.ts
+++ b/src/i18n/lang/ko-KR/comp.ts
@@ -51,6 +51,31 @@ export default {
copyFailed: '복사 실패',
backgroundDownload: '백그라운드 다운로드'
},
+ disclaimer: {
+ title: '이용 안내',
+ warning:
+ '본 앱은 개발 테스트 버전으로 기능이 아직 미흡하며, 다수의 문제와 버그가 존재할 수 있습니다. 학습 및 교류 목적으로만 사용하십시오.',
+ item1:
+ '본 앱은 개인의 학습, 연구 및 기술 교류 목적으로만 사용되며, 상업적 용도로 사용하지 마십시오.',
+ item2:
+ '다운로드 후 24시간 이내에 삭제해 주십시오. 장기 사용을 원하시면 정품 음악 서비스를 이용해 주십시오.',
+ item3:
+ '본 앱을 사용함으로써 관련 위험을 이해하고 감수하는 것으로 간주합니다. 개발자는 어떠한 손실에 대해서도 책임을 지지 않습니다.',
+ agree: '숙지하였으며 이에 동의합니다',
+ disagree: '동의하지 않음 및 정지'
+ },
+ donate: {
+ title: '개발자 지원',
+ subtitle: '여러분의 지원이 저의 원동력입니다',
+ tip: '후원은 완전히 자율적입니다. 후원하지 않더라도 모든 기능을 정상적으로 사용할 수 있습니다. 이해와 지원에 감사드립니다!',
+ wechat: 'WeChat',
+ alipay: 'Alipay',
+ wechatQR: 'WeChat 결제 코드',
+ alipayQR: 'Alipay 결제 코드',
+ scanTip: '휴대전화로 위 QR 코드를 스캔하여 후원해 주세요',
+ enterApp: '앱 시작하기',
+ noForce: '후원은 강제가 아닙니다. 클릭하여 시작할 수 있습니다'
+ },
coffee: {
title: '커피 한 잔 사주세요',
alipay: '알리페이',
diff --git a/src/i18n/lang/zh-CN/comp.ts b/src/i18n/lang/zh-CN/comp.ts
index 7a64ee5..f15425f 100644
--- a/src/i18n/lang/zh-CN/comp.ts
+++ b/src/i18n/lang/zh-CN/comp.ts
@@ -50,6 +50,27 @@ export default {
copyFailed: '复制失败',
backgroundDownload: '后台下载'
},
+ disclaimer: {
+ title: '使用须知',
+ warning: '本应用为开发测试版本,功能尚不完善,可能存在较多问题和 Bug,仅供学习交流使用。',
+ item1: '本应用仅供个人学习、研究和技术交流使用,请勿用于任何商业用途。',
+ item2: '请在下载后 24 小时内删除,如需长期使用请支持正版音乐服务。',
+ item3: '使用本应用即表示您理解并承担相关风险,开发者不对任何损失负责。',
+ agree: '我已阅读并同意',
+ disagree: '不同意并退出'
+ },
+ donate: {
+ title: '支持开发者',
+ subtitle: '您的支持是我前进的动力',
+ tip: '捐赠完全自愿,不捐赠也可以正常使用所有功能,感谢您的理解与支持!',
+ wechat: '微信',
+ alipay: '支付宝',
+ wechatQR: '微信收款码',
+ alipayQR: '支付宝收款码',
+ scanTip: '请使用手机扫描上方二维码进行捐赠',
+ enterApp: '进入应用',
+ noForce: '不强制捐赠,点击即可进入'
+ },
coffee: {
title: '请我喝咖啡',
alipay: '支付宝',
diff --git a/src/i18n/lang/zh-Hant/comp.ts b/src/i18n/lang/zh-Hant/comp.ts
index eca7605..5e37981 100644
--- a/src/i18n/lang/zh-Hant/comp.ts
+++ b/src/i18n/lang/zh-Hant/comp.ts
@@ -50,6 +50,27 @@ export default {
copyFailed: '複製失敗',
backgroundDownload: '背景下載'
},
+ disclaimer: {
+ title: '使用說明',
+ warning: '本程式為開發測試版本,功能尚未完善,可能存在諸多問題及臭蟲,僅供學習交流使用。',
+ item1: '本程式僅供個人學習、研究及技術交流之目的,不得用於任何商業用途。',
+ item2: '請在下載後 24 小時內刪除,若對您有所幫助,請支持正版音樂。',
+ item3: '使用本程式即代表您已了解並同意相關風險,開發者對任何損失概不負責。',
+ agree: '我已了解並同意',
+ disagree: '不同意並退出'
+ },
+ donate: {
+ title: '支援開發者',
+ subtitle: '您的支援是我持續更新的動力',
+ tip: '捐贈完全採自願原則。即使不捐贈,您依然可以正常使用所有功能。感謝您的理解與支援!',
+ wechat: '微信支付',
+ alipay: '支付寶',
+ wechatQR: '微信收款碼',
+ alipayQR: '支付寶收款碼',
+ scanTip: '請使用手機 App 掃描 QR Code 進行捐贈',
+ enterApp: '進入程式',
+ noForce: '捐贈並非強制,您可以點擊按鈕直接進入'
+ },
coffee: {
title: '請我喝杯咖啡',
alipay: '支付寶',
diff --git a/src/main/modules/window.ts b/src/main/modules/window.ts
index f32f305..06e66aa 100644
--- a/src/main/modules/window.ts
+++ b/src/main/modules/window.ts
@@ -143,6 +143,12 @@ export function initializeWindowManager() {
}
});
+ // 强制退出应用(用于免责声明拒绝等场景)
+ ipcMain.on('quit-app', () => {
+ setAppQuitting(true);
+ app.quit();
+ });
+
ipcMain.on('mini-tray', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
diff --git a/src/main/server.ts b/src/main/server.ts
index a9a0488..e13dc81 100644
--- a/src/main/server.ts
+++ b/src/main/server.ts
@@ -23,14 +23,57 @@ ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) =>
}
});
-async function startMusicApi(): Promise {
- console.log('MUSIC API STARTED');
-
- const port = (store.get('set') as any).musicApiPort || 30488;
-
- await server.serveNcmApi({
- port
+/**
+ * 检查端口是否可用
+ */
+function checkPortAvailable(port: number): Promise {
+ return new Promise((resolve) => {
+ const net = require('net');
+ const tester = net
+ .createServer()
+ .once('error', () => {
+ resolve(false);
+ })
+ .once('listening', () => {
+ tester.close(() => resolve(true));
+ })
+ .listen(port);
});
}
+async function startMusicApi(): Promise {
+ console.log('MUSIC API STARTING...');
+
+ const settings = store.get('set') as any;
+ let port = settings?.musicApiPort || 30488;
+ const maxRetries = 10;
+
+ // 检查端口是否可用,如果不可用则尝试下一个端口
+ for (let i = 0; i < maxRetries; i++) {
+ const isAvailable = await checkPortAvailable(port);
+ if (isAvailable) {
+ break;
+ }
+ console.log(`端口 ${port} 被占用,尝试切换到端口 ${port + 1}`);
+ port++;
+ }
+
+ // 如果端口发生变化,保存新端口到配置
+ const originalPort = settings?.musicApiPort || 30488;
+ if (port !== originalPort) {
+ console.log(`端口从 ${originalPort} 切换到 ${port}`);
+ store.set('set', { ...settings, musicApiPort: port });
+ }
+
+ try {
+ await server.serveNcmApi({
+ port
+ });
+ console.log(`MUSIC API STARTED on port ${port}`);
+ } catch (error) {
+ console.error(`MUSIC API 启动失败:`, error);
+ throw error;
+ }
+}
+
export { startMusicApi };
diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts
index 95e0592..3f86bcd 100644
--- a/src/preload/index.d.ts
+++ b/src/preload/index.d.ts
@@ -4,6 +4,7 @@ interface API {
minimize: () => void;
maximize: () => void;
close: () => void;
+ quitApp: () => void;
dragStart: (data: any) => void;
miniTray: () => void;
miniWindow: () => void;
@@ -25,11 +26,7 @@ interface API {
importLxMusicScript: () => Promise<{ name: string; content: string } | null>;
invoke: (channel: string, ...args: any[]) => Promise;
getSearchSuggestions: (keyword: string) => Promise;
- lxMusicHttpRequest: (request: {
- url: string;
- options: any;
- requestId: string;
- }) => Promise;
+ lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) => Promise;
lxMusicHttpCancel: (requestId: string) => Promise;
}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index ef27906..1f3e7de 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -6,6 +6,7 @@ const api = {
minimize: () => ipcRenderer.send('minimize-window'),
maximize: () => ipcRenderer.send('maximize-window'),
close: () => ipcRenderer.send('close-window'),
+ quitApp: () => ipcRenderer.send('quit-app'),
dragStart: (data) => ipcRenderer.send('drag-start', data),
miniTray: () => ipcRenderer.send('mini-tray'),
miniWindow: () => ipcRenderer.send('mini-window'),
@@ -61,11 +62,8 @@ const api = {
getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword),
// 落雪音乐 HTTP 请求(绕过 CORS)
- lxMusicHttpRequest: (request: {
- url: string;
- options: any;
- requestId: string;
- }) => ipcRenderer.invoke('lx-music-http-request', request),
+ lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) =>
+ ipcRenderer.invoke('lx-music-http-request', request),
lxMusicHttpCancel: (requestId: string) => ipcRenderer.invoke('lx-music-http-cancel', requestId)
};
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
index 58b7fbc..e07e82a 100644
--- a/src/renderer/App.vue
+++ b/src/renderer/App.vue
@@ -5,6 +5,7 @@
+
@@ -18,6 +19,7 @@ import { computed, nextTick, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
+import DisclaimerModal from '@/components/common/DisclaimerModal.vue';
import TrafficWarningDrawer from '@/components/TrafficWarningDrawer.vue';
import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
diff --git a/src/renderer/components/common/DisclaimerModal.vue b/src/renderer/components/common/DisclaimerModal.vue
index 56b39d0..a0912ab 100644
--- a/src/renderer/components/common/DisclaimerModal.vue
+++ b/src/renderer/components/common/DisclaimerModal.vue
@@ -237,6 +237,7 @@ import { useI18n } from 'vue-i18n';
// 导入收款码图片
import alipayQRCode from '@/assets/alipay.png';
import wechatQRCode from '@/assets/wechat.png';
+import { isElectron, isLyricWindow } from '@/utils';
const { t } = useI18n();
@@ -250,20 +251,8 @@ const qrcodeType = ref<'wechat' | 'alipay'>('wechat');
const isTransitioning = ref(false); // 防止用户点击过快
// 检查是否需要显示免责声明
-const shouldShowDisclaimer = (): boolean => {
- const agreedTime = localStorage.getItem(DISCLAIMER_AGREED_KEY);
-
- // 从未同意过
- if (!agreedTime) return true;
-
- const savedTime = parseInt(agreedTime, 10);
- const now = Date.now();
-
- // 随机 3-10 天后再次显示
- const randomDays = Math.floor(Math.random() * 8) + 3; // 3-10 天
- const intervalMs = randomDays * 24 * 60 * 60 * 1000;
-
- return now - savedTime >= intervalMs;
+const shouldShowDisclaimer = () => {
+ return !localStorage.getItem(DISCLAIMER_AGREED_KEY);
};
// 处理同意
@@ -278,13 +267,18 @@ const handleAgree = () => {
}, 300);
};
-// 处理不同意 - 关闭窗口
+// 处理不同意 - 退出应用
const handleDisagree = () => {
if (isTransitioning.value) return;
isTransitioning.value = true;
- // Web 环境下尝试关闭窗口
- window.close();
+ if (isElectron) {
+ // Electron 环境下强制退出应用
+ window.api?.quitApp?.();
+ } else {
+ // Web 环境下尝试关闭窗口
+ window.close();
+ }
isTransitioning.value = false;
};
@@ -316,6 +310,9 @@ const handleEnterApp = () => {
};
onMounted(() => {
+ // 歌词窗口不显示免责声明
+ if (isLyricWindow.value) return;
+
// 检查是否需要显示免责声明
if (shouldShowDisclaimer()) {
showDisclaimer.value = true;