diff --git a/.gitignore b/.gitignore index 408564e..256a172 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,8 @@ bin/ auth_storage/ www neutralino.js -package-lock.json \ No newline at end of file +package-lock.json +# Direnv +.envrc +# vim +.lazy.lua diff --git a/android/.gitignore b/android/.gitignore index 48354a3..409e290 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -99,3 +99,7 @@ app/src/main/assets/public app/src/main/assets/capacitor.config.json app/src/main/assets/capacitor.plugins.json app/src/main/res/xml/config.xml + +# Generated by LS +.settings/ +.project diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 4e379a3..fe3fb30 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') implementation project(':capacitor-haptics') + implementation project(':capgo-capacitor-media-session') } diff --git a/android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java b/android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java index 42a6b43..96bc39d 100644 --- a/android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java +++ b/android/app/src/main/java/tf/monochrome/music/BackgroundAudioPlugin.java @@ -32,7 +32,7 @@ public class BackgroundAudioPlugin extends Plugin { } } - `@PluginMethod` + @PluginMethod public void stop(PluginCall call) { try { Intent intent = new Intent(getContext(), AudioPlaybackService.class); diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index df5cd63..0603a5b 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -7,3 +7,6 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/ include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') + +include ':capgo-capacitor-media-session' +project(':capgo-capacitor-media-session').projectDir = new File('../node_modules/@capgo/capacitor-media-session/android') diff --git a/bun.lock b/bun.lock index 1890ea8..2e5156f 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@capacitor/core": "^8.2.0", "@capacitor/haptics": "^8.0.1", "@capacitor/ios": "^8.2.0", + "@capgo/capacitor-media-session": "^8.0.19", "@dantheman827/taglib-ts": "^0.1.5", "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", @@ -277,6 +278,8 @@ "@capacitor/ios": ["@capacitor/ios@8.3.0", "", { "peerDependencies": { "@capacitor/core": "^8.3.0" } }, "sha512-5Rtwv8SITKlYTt8lAZG+khnVIdzPtqbocH3eP+JkEmX1vpSMwx4TOKtT8OBz8gpQ+pUJDRp7DBYOv3U6l/obCw=="], + "@capgo/capacitor-media-session": ["@capgo/capacitor-media-session@8.0.19", "", { "peerDependencies": { "@capacitor/core": ">=8.0.0" } }, "sha512-m0eCJvnuYpxz3wj3Snc1kIHCr45XQOBgPwOhkId/xXZ01DzXsOpw/lIT8Fl/Y1AscpNxs2fF5Qoj4E8QMIyJJg=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260401.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZSmceM70jH6k+/62VkEcmMNzrpr4kSctkX5Lsgqv38KktfhPY/hsh75y1lRoPWS3H3kgMa4p2pUSlidZR1u2hw=="], "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260401.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7UKWF+IUZ3NXMVPsDg8Cjg0r58b+uYlfvs5Yt8bvtU+geCtW4P2MxRHmRSEo8SryckXOJjb/b8tcncgCykFu8g=="], diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index 80f1c8f..b94a368 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -11,9 +11,10 @@ let package = Package( targets: ["CapApp-SPM"]) ], dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.2.0"), + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.0"), .package(name: "CapacitorApp", path: "../../../node_modules/@capacitor/app"), - .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics") + .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics"), + .package(name: "CapgoCapacitorMediaSession", path: "../../../node_modules/@capgo/capacitor-media-session") ], targets: [ .target( @@ -22,7 +23,8 @@ let package = Package( .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), .product(name: "CapacitorApp", package: "CapacitorApp"), - .product(name: "CapacitorHaptics", package: "CapacitorHaptics") + .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), + .product(name: "CapgoCapacitorMediaSession", package: "CapgoCapacitorMediaSession") ] ) ] diff --git a/js/player.js b/js/player.js index b9023a4..159d48d 100644 --- a/js/player.js +++ b/js/player.js @@ -24,6 +24,7 @@ import { db } from './db.js'; import { SVG_CLOCK, SVG_ATMOS } from './icons.js'; import { UIRenderer } from './ui.js'; +import { MediaSession } from '@capgo/capacitor-media-session'; export class Player { static #instance = null; @@ -423,10 +424,8 @@ export class Player { } async setupMediaSession() { - if (!('mediaSession' in navigator)) return; - const setHandlers = async () => { - navigator.mediaSession.setActionHandler('play', async () => { + await MediaSession.setActionHandler({ action: 'play' }, async () => { const el = this.activeElement; // Initialize and resume audio context first (required for iOS lock screen) // Must happen before audio.play() or audio won't route through Web Audio @@ -445,11 +444,11 @@ export class Player { } }); - navigator.mediaSession.setActionHandler('pause', () => { + await MediaSession.setActionHandler({ action: 'pause' }, () => { this.activeElement.pause(); }); - navigator.mediaSession.setActionHandler('previoustrack', async () => { + await MediaSession.setActionHandler({ action: 'previoustrack' }, async () => { // Ensure audio context is active for iOS lock screen controls if (!audioContextManager.isReady()) { audioContextManager.init(this.activeElement); @@ -459,7 +458,7 @@ export class Player { this.playPrev(); }); - navigator.mediaSession.setActionHandler('nexttrack', async () => { + await MediaSession.setActionHandler({ action: 'nexttrack' }, async () => { // Ensure audio context is active for iOS lock screen controls if (!audioContextManager.isReady()) { audioContextManager.init(this.activeElement); @@ -470,24 +469,24 @@ export class Player { }); if (!this.isIOS) { - navigator.mediaSession.setActionHandler('seekbackward', (details) => { + await MediaSession.setActionHandler({ action: 'seekbackward' }, (details) => { const skipTime = details.seekOffset || 10; this.seekBackward(skipTime); }); - navigator.mediaSession.setActionHandler('seekforward', (details) => { + await MediaSession.setActionHandler({ action: 'seekforward' }, (details) => { const skipTime = details.seekOffset || 10; this.seekForward(skipTime); }); } - navigator.mediaSession.setActionHandler('seekto', (details) => { + await MediaSession.setActionHandler({ action: 'seekto' }, (details) => { if (details.seekTime !== undefined) { this.activeElement.currentTime = Math.max(0, details.seekTime); this.updateMediaSessionPositionState(); } }); - navigator.mediaSession.setActionHandler('stop', () => { + await MediaSession.setActionHandler({ action: 'stop' }, () => { this.activeElement.pause(); this.activeElement.currentTime = 0; this.updateMediaSessionPlaybackState(); @@ -2091,37 +2090,38 @@ export class Player { } updateMediaSession(track) { - if (!('mediaSession' in navigator)) return; - - // Force a refresh for picky Bluetooth systems by clearing metadata first - navigator.mediaSession.metadata = null; const coverId = track.album?.cover; const trackTitle = getTrackTitle(track); - navigator.mediaSession.metadata = new MediaMetadata({ - title: trackTitle || 'Unknown Title', - artist: getTrackArtists(track) || 'Unknown Artist', - album: track.album?.title || 'Unknown Album', - artwork: coverId - ? [ - { - src: this.api.getCoverUrl(coverId, '1280'), - sizes: '1280x1280', - type: 'image/jpeg', - }, - ] - : undefined, + // Force a refresh for picky Bluetooth systems by clearing metadata first + MediaSession.setMetadata({}) + .finally(() => + MediaSession.setMetadata({ + title: trackTitle || 'Unknown Title', + artist: getTrackArtists(track) || 'Unknown Artist', + album: track.album?.title || 'Unknown Album', + artwork: coverId + ? [ + { + src: this.api.getCoverUrl(coverId, '1280'), + sizes: '1280x1280', + type: 'image/jpeg', + }, + ] + : undefined, + }) + ) + .catch(() => {}) + .finally(() => { + this.updateMediaSessionPlaybackState(); + this.updateMediaSessionPositionState(); }); - - this.updateMediaSessionPlaybackState(); - this.updateMediaSessionPositionState(); } updateMediaSessionPlaybackState() { - if (!('mediaSession' in navigator)) return; const isPlaying = !this.activeElement.paused; - navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'; + void MediaSession.setPlaybackState({ playbackState: isPlaying ? 'playing' : 'paused' }); // Start/stop Android foreground service to prevent background audio throttling this._updateBackgroundAudioService(isPlaying); @@ -2158,9 +2158,6 @@ export class Player { } updateMediaSessionPositionState() { - if (!('mediaSession' in navigator)) return; - if (!('setPositionState' in navigator.mediaSession)) return; - const el = this.activeElement; const duration = el.duration; @@ -2168,15 +2165,14 @@ export class Player { return; } - try { - navigator.mediaSession.setPositionState({ - duration: duration, - playbackRate: el.playbackRate || 1, - position: Math.min(el.currentTime, duration), - }); - } catch (error) { - console.log('Failed to update Media Session position:', error); - } + MediaSession.setPositionState({ + duration: duration, + playbackRate: el.playbackRate || 1, + position: Math.min(el.currentTime, duration), + }) + .catch((error) => { + console.log('Failed to update Media Session position:', error); + }); } async safePlay(element = this.activeElement) { diff --git a/package.json b/package.json index 2ca4671..911d9a0 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "type": "git", "url": "git+https://github.com/monochrome-music/monochrome.git" }, - "keywords": [], + "keywords": ["music"], "author": "", - "license": "ISC", + "license": "Apache-2.0", "bugs": { "url": "https://github.com/monochrome-music/monochrome/issues" }, @@ -68,6 +68,7 @@ "@capacitor/core": "^8.2.0", "@capacitor/haptics": "^8.0.1", "@capacitor/ios": "^8.2.0", + "@capgo/capacitor-media-session": "^8.0.19", "@dantheman827/taglib-ts": "^0.1.5", "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", @@ -76,7 +77,7 @@ "@svta/common-media-library": "^0.18.1", "@types/wicg-file-system-access": "^2023.10.7", "@typescript-eslint/eslint-plugin": "^8.57.2", - "@uimaxbai/am-lyrics": "^1.2.7", + "@uimaxbai/am-lyrics": "^1.2.8", "@vitest/web-worker": "^4.1.2", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", diff --git a/styles.css b/styles.css index 0d1df0b..7605627 100644 --- a/styles.css +++ b/styles.css @@ -2577,7 +2577,6 @@ body.multi-select-mode .track-item:hover { min-height: 550px; display: flex; align-items: flex-end; - background-color: var(--card); } @media (max-width: 1024px) {