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;