Skip to content

Commit 99397cc

Browse files
authored
Merge pull request #1614 from spiicez21/feature/desktop-mini-player
Feature/desktop mini player
2 parents 594221a + cdfb6ad commit 99397cc

File tree

12 files changed

+1170
-51
lines changed

12 files changed

+1170
-51
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ crowdin.yml
2121
/simpmusic.jks
2222
/ffmpeg-kit/build/
2323

24+
#dev files
25+
ign.md
26+
.docs
27+
2428
# Sentry Config File
2529
sentry.properties
2630
/.claude/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.maxrave.simpmusic.expect
2+
3+
// No-op on Android - mini player is desktop only
4+
actual fun toggleMiniPlayer() {
5+
// Do nothing on Android
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.maxrave.simpmusic.expect
2+
3+
/**
4+
* Platform-specific function to toggle mini player window.
5+
* Only implemented on Desktop (JVM), no-op on other platforms.
6+
*/
7+
expect fun toggleMiniPlayer()

composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/MiniPlayer.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import androidx.compose.foundation.layout.wrapContentSize
4040
import androidx.compose.foundation.shape.CircleShape
4141
import androidx.compose.foundation.shape.RoundedCornerShape
4242
import androidx.compose.material.icons.Icons
43+
import androidx.compose.material.icons.filled.VolumeOff
44+
import androidx.compose.material.icons.filled.VolumeUp
45+
import androidx.compose.material.icons.outlined.OpenInNew
4346
import androidx.compose.material.icons.rounded.Close
4447
import androidx.compose.material.icons.rounded.Speaker
4548
import androidx.compose.material3.Card
@@ -99,6 +102,7 @@ import com.maxrave.simpmusic.expect.ui.toImageBitmap
99102
import com.maxrave.simpmusic.extension.formatDuration
100103
import com.maxrave.simpmusic.extension.getColorFromPalette
101104
import com.maxrave.simpmusic.extension.toResizedBitmap
105+
import com.maxrave.simpmusic.expect.toggleMiniPlayer
102106
import com.maxrave.simpmusic.getPlatform
103107
import com.maxrave.simpmusic.ui.component.ExplicitBadge
104108
import com.maxrave.simpmusic.ui.component.HeartCheckBox
@@ -807,7 +811,30 @@ fun MiniPlayer(
807811
sharedViewModel.onUIEvent(UIEvent.ToggleLike)
808812
}
809813
Spacer(Modifier.width(8.dp))
810-
Icon(Icons.Rounded.Speaker, "")
814+
// Desktop mini player button (JVM only)
815+
if (getPlatform() == Platform.Desktop) {
816+
IconButton(onClick = { toggleMiniPlayer() }) {
817+
Icon(
818+
imageVector = Icons.Outlined.OpenInNew,
819+
contentDescription = "Mini Player"
820+
)
821+
}
822+
}
823+
IconButton(
824+
onClick = {
825+
// Toggle mute/unmute
826+
val newVolume = if (controllerState.volume > 0f) 0f else 1f
827+
sharedViewModel.onUIEvent(UIEvent.UpdateVolume(newVolume))
828+
}
829+
) {
830+
Icon(
831+
imageVector = if (controllerState.volume > 0f)
832+
Icons.Filled.VolumeUp
833+
else
834+
Icons.Filled.VolumeOff,
835+
contentDescription = if (controllerState.volume > 0f) "Mute" else "Unmute"
836+
)
837+
}
811838
Spacer(Modifier.width(4.dp))
812839
var isVolumeSliding by rememberSaveable {
813840
mutableStateOf(false)

composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import androidx.compose.material.icons.automirrored.rounded.QueueMusic
5656
import androidx.compose.material.icons.filled.Subtitles
5757
import androidx.compose.material.icons.filled.SubtitlesOff
5858
import androidx.compose.material.icons.outlined.Info
59+
import androidx.compose.material.icons.outlined.OpenInNew
5960
import androidx.compose.material.icons.rounded.AddCircleOutline
6061
import androidx.compose.material.icons.rounded.CheckCircle
6162
import androidx.compose.material.icons.rounded.Forward5
@@ -125,6 +126,7 @@ import com.kmpalette.rememberPaletteState
125126
import com.maxrave.common.Config.MAIN_PLAYER
126127
import com.maxrave.logger.Logger
127128
import com.maxrave.simpmusic.Platform
129+
import com.maxrave.simpmusic.expect.toggleMiniPlayer
128130
import com.maxrave.simpmusic.expect.ui.MediaPlayerView
129131
import com.maxrave.simpmusic.expect.ui.MediaPlayerViewWithSubtitle
130132
import com.maxrave.simpmusic.extension.GradientAngle
@@ -731,6 +733,16 @@ fun NowPlayingScreenContent(
731733
}
732734
},
733735
actions = {
736+
// Desktop mini player button (JVM only)
737+
if (getPlatform() == Platform.Desktop) {
738+
IconButton(onClick = { toggleMiniPlayer() }) {
739+
Icon(
740+
imageVector = Icons.Outlined.OpenInNew,
741+
contentDescription = "Mini Player",
742+
tint = Color.White,
743+
)
744+
}
745+
}
734746
IconButton(onClick = {
735747
showSheet = true
736748
}) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.maxrave.simpmusic.expect
2+
3+
// No-op on iOS - mini player is desktop only
4+
actual fun toggleMiniPlayer() {
5+
// Do nothing on iOS
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.maxrave.simpmusic.expect
2+
3+
import com.maxrave.logger.Logger
4+
import com.maxrave.simpmusic.ui.mini_player.MiniPlayerManager
5+
6+
actual fun toggleMiniPlayer() {
7+
Logger.d("MiniPlayer", "Toggle called, current state: ${MiniPlayerManager.isOpen}")
8+
MiniPlayerManager.isOpen = !MiniPlayerManager.isOpen
9+
Logger.d("MiniPlayer", "New state: ${MiniPlayerManager.isOpen}")
10+
}

composeApp/src/jvmMain/kotlin/com/maxrave/simpmusic/main.kt

Lines changed: 67 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import com.maxrave.domain.manager.DataStoreManager
1818
import com.maxrave.domain.mediaservice.handler.MediaPlayerHandler
1919
import com.maxrave.domain.mediaservice.handler.ToastType
2020
import com.maxrave.simpmusic.di.viewModelModule
21+
import com.maxrave.simpmusic.ui.mini_player.MiniPlayerManager
22+
import com.maxrave.simpmusic.ui.mini_player.MiniPlayerWindow
2123
import com.maxrave.simpmusic.utils.VersionManager
2224
import com.maxrave.simpmusic.viewModel.SharedViewModel
2325
import com.maxrave.simpmusic.viewModel.changeLanguageNative
@@ -41,62 +43,66 @@ import simpmusic.composeapp.generated.resources.explicit_content_blocked
4143
import simpmusic.composeapp.generated.resources.time_out_check_internet_connection_or_change_piped_instance_in_settings
4244

4345
@OptIn(ExperimentalMaterial3Api::class)
44-
fun main() =
45-
application {
46-
System.setProperty("compose.swing.render.on.graphics", "true")
47-
System.setProperty("compose.interop.blending", "true")
48-
System.setProperty("compose.layers.type", "COMPONENT")
49-
startKoin {
50-
loadAllModules()
51-
loadKoinModules(viewModelModule)
46+
fun main() {
47+
System.setProperty("compose.swing.render.on.graphics", "true")
48+
System.setProperty("compose.interop.blending", "true")
49+
System.setProperty("compose.layers.type", "COMPONENT")
50+
51+
// Initialize Koin ONCE before application starts
52+
startKoin {
53+
loadAllModules()
54+
loadKoinModules(viewModelModule)
55+
}
56+
57+
val language =
58+
runBlocking {
59+
getKoin()
60+
.get<DataStoreManager>()
61+
.language
62+
.first()
63+
.substring(0..1)
5264
}
53-
val language =
54-
runBlocking {
55-
getKoin()
56-
.get<DataStoreManager>()
57-
.language
58-
.first()
59-
.substring(0..1)
60-
}
61-
changeLanguageNative(language)
65+
changeLanguageNative(language)
66+
67+
VersionManager.initialize()
68+
if (BuildKonfig.sentryDsn.isNotEmpty()) {
69+
Sentry.init { options ->
70+
options.dsn = BuildKonfig.sentryDsn
71+
options.release = "simpmusic-desktop@${VersionManager.getVersionName()}"
72+
options.setDiagnosticLevel(SentryLevel.ERROR)
73+
}
74+
}
75+
76+
val mediaPlayerHandler by inject<MediaPlayerHandler>(MediaPlayerHandler::class.java)
77+
mediaPlayerHandler.showToast = { type ->
78+
showToast(
79+
when (type) {
80+
ToastType.ExplicitContent -> runBlocking { getString(Res.string.explicit_content_blocked) }
81+
is ToastType.PlayerError ->
82+
runBlocking { getString(Res.string.time_out_check_internet_connection_or_change_piped_instance_in_settings, type.error) }
83+
},
84+
)
85+
}
86+
mediaPlayerHandler.pushPlayerError = { error ->
87+
Sentry.withScope { scope ->
88+
Sentry.captureMessage("Player Error: ${error.message}, code: ${error.errorCode}, code name: ${error.errorCodeName}")
89+
}
90+
}
91+
92+
val sharedViewModel = getKoin().get<SharedViewModel>()
93+
if (sharedViewModel.shouldCheckForUpdate()) {
94+
sharedViewModel.checkForUpdate()
95+
}
96+
97+
application {
6298
val windowState =
6399
rememberWindowState(
64100
size = DpSize(1280.dp, 720.dp),
65101
)
66-
VersionManager.initialize()
67-
if (BuildKonfig.sentryDsn.isNotEmpty()) {
68-
Sentry.init { options ->
69-
options.dsn = BuildKonfig.sentryDsn
70-
options.release = "simpmusic-desktop@${VersionManager.getVersionName()}"
71-
options.setDiagnosticLevel(SentryLevel.ERROR)
72-
}
73-
}
74-
val mediaPlayerHandler by inject<MediaPlayerHandler>(MediaPlayerHandler::class.java)
75-
mediaPlayerHandler.showToast = { type ->
76-
showToast(
77-
when (type) {
78-
ToastType.ExplicitContent -> runBlocking { getString(Res.string.explicit_content_blocked) }
79-
is ToastType.PlayerError ->
80-
runBlocking { getString(Res.string.time_out_check_internet_connection_or_change_piped_instance_in_settings, type.error) }
81-
},
82-
)
83-
}
84-
mediaPlayerHandler.pushPlayerError = { error ->
85-
Sentry.withScope { scope ->
86-
Sentry.captureMessage("Player Error: ${error.message}, code: ${error.errorCode}, code name: ${error.errorCodeName}")
87-
}
88-
}
89-
val onExitApplication: () -> Unit = {
90-
mediaPlayerHandler.release()
91-
exitApplication()
92-
}
93-
val sharedViewModel = getKoin().get<SharedViewModel>()
94-
if (sharedViewModel.shouldCheckForUpdate()) {
95-
sharedViewModel.checkForUpdate()
96-
}
97102
Window(
98103
onCloseRequest = {
99-
onExitApplication()
104+
mediaPlayerHandler.release()
105+
exitApplication()
100106
},
101107
title = "SimpMusic",
102108
icon = painterResource(Res.drawable.circle_app_icon),
@@ -130,4 +136,15 @@ fun main() =
130136
App()
131137
ToastHost()
132138
}
133-
}
139+
140+
// Mini Player Window (separate window)
141+
if (MiniPlayerManager.isOpen) {
142+
MiniPlayerWindow(
143+
sharedViewModel = sharedViewModel,
144+
onCloseRequest = {
145+
MiniPlayerManager.isOpen = false
146+
}
147+
)
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)