Skip to content

Commit e90489d

Browse files
committed
feat: Implement Radio Browser API with server status monitoring
- Add RadioBrowserApi class with robust server discovery and mirror management - Implement server status monitoring with detailed health checks - Add support for both IPv4 and IPv6 server addresses - Create ServerStatusView component for monitoring server health - Integrate server status view into main UI with toggle functionality - Add proper error handling and retry mechanisms - Implement search functionality for radio stations - Add support for filtering stations by country, language, and tags Technical details: - Use Ktor client for HTTP requests with proper timeout handling - Implement coroutine-based async operations - Add comprehensive logging for debugging - Use Material Design components for UI - Implement proper state management with ViewModel - Add retry mechanism for failed requests - Support both IPv4 and IPv6 server addresses with proper URL formatting UI Components: - Add ServerStatusView for monitoring server health - Create StationCard for displaying radio station information - Implement search bar with real-time updates - Add loading and error states - Support toggling between server status and radio content This commit establishes the core functionality of the Radio Browser application with proper server management and user interface.
1 parent 630658e commit e90489d

File tree

21 files changed

+817
-533
lines changed

21 files changed

+817
-533
lines changed

.idea/misc.xml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composeApp/build.gradle.kts

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,29 @@ plugins {
55
alias(libs.plugins.jetbrainsCompose)
66
}
77

8+
repositories {
9+
mavenCentral()
10+
google()
11+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
12+
maven("https://jitpack.io")
13+
}
14+
815
kotlin {
9-
jvm("desktop")
16+
jvm("desktop") {
17+
compilations.all {
18+
kotlinOptions.jvmTarget = "17"
19+
}
20+
}
21+
1022
sourceSets {
11-
val desktopMain by getting
12-
val koin_version = "3.2.0"
23+
val desktopMain by getting {
24+
dependencies {
25+
implementation(compose.desktop.currentOs)
26+
implementation("ch.qos.logback:logback-classic:1.4.11")
27+
implementation("org.slf4j:slf4j-api:2.0.9")
28+
}
29+
}
30+
val koin_version = "4.0.3"
1331

1432
commonMain.dependencies {
1533
implementation(compose.runtime)
@@ -18,38 +36,53 @@ kotlin {
1836
implementation(compose.ui)
1937
implementation(compose.components.resources)
2038
implementation(compose.components.uiToolingPreview)
21-
implementation("de.sfuhrm:radiobrowser4j:3.0.0")
2239
implementation("com.darkrockstudios:mpfilepicker:3.1.0")
2340

24-
implementation("io.insert-koin:koin-core:$koin_version")
41+
// Koin
42+
implementation(platform("io.insert-koin:koin-bom:$koin_version"))
43+
implementation("io.insert-koin:koin-core")
44+
implementation("io.insert-koin:koin-core-coroutines")
45+
implementation("io.insert-koin:koin-compose")
46+
implementation("io.insert-koin:koin-compose-viewmodel")
47+
implementation("io.insert-koin:koin-logger-slf4j")
48+
49+
// Ktor for HTTP requests
50+
implementation("io.ktor:ktor-client-core:2.3.8")
51+
implementation("io.ktor:ktor-client-cio:2.3.8")
52+
implementation("io.ktor:ktor-client-content-negotiation:2.3.8")
53+
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
54+
2555
api("io.github.qdsfdhvh:image-loader:1.7.8")
2656
api("io.github.qdsfdhvh:image-loader-extension-moko-resources:1.7.8")
27-
// implementation("io.insert-koin:koin-core-coroutines:$koin_version")
28-
// implementation("io.insert-koin:koin-compose:$koin_version")
29-
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
30-
31-
32-
}
33-
desktopMain.dependencies {
34-
implementation(compose.desktop.currentOs)
3557
}
3658
}
3759
}
3860

39-
4061
compose.desktop {
4162
application {
42-
mainClass = "MainKt"
63+
mainClass = "main.MainKt"
64+
jvmArgs += listOf("-Xmx2G")
65+
buildTypes {
66+
release {
67+
proguard {
68+
configurationFiles.from("compose-desktop.pro")
69+
}
70+
}
71+
}
4372
nativeDistributions {
4473
targetFormats(TargetFormat.Exe)
74+
packageName = "RadioBrowser"
75+
packageVersion = "1.0.0"
4576
windows {
4677
iconFile.set(project.file("src/commonMain/composeResources/drawable/icon.ico"))
47-
shortcut = true
4878
}
49-
appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))
50-
includeAllModules = true
51-
packageName = "RadioBrowser"
52-
packageVersion = "1.0.2"
5379
}
5480
}
5581
}
82+
83+
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
84+
kotlinOptions {
85+
jvmTarget = "17"
86+
freeCompilerArgs = listOf("-Xjsr305=strict")
87+
}
88+
}

composeApp/src/desktopMain/kotlin/main.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,18 @@
11
package main.di
22

3-
import org.koin.core.module.dsl.bind
4-
import org.koin.core.module.dsl.createdAtStart
5-
import org.koin.core.module.dsl.singleOf
6-
import org.koin.core.module.dsl.withOptions
7-
import player.data.repository.PlayerImpl
8-
import org.koin.core.qualifier.named
93
import org.koin.dsl.module
4+
import player.data.repository.PlayerImpl
105
import player.domain.repository.PlayerRepository
116
import player.presentation.viewmodel.PlayerViewModel
12-
13-
14-
val SharedModule = module(createdAtStart=true) {
15-
16-
// single<PlayerRepository>(named("playerRepository")) {
17-
// PlayerImpl()
18-
// }
19-
20-
singleOf(::PlayerImpl) withOptions {
21-
// definition options
22-
named("playerRepository")
23-
bind<PlayerRepository>()
24-
createdAtStart()
25-
}
26-
27-
28-
single<PlayerViewModel>{
29-
PlayerViewModel()
30-
} withOptions {
31-
// definition options
32-
named("playerViewModel")
33-
createdAtStart()
34-
}
35-
36-
37-
38-
39-
// single<PlayerRepository> { PlayerImpl() as PlayerRepository } withOptions {
40-
// named("playerRepository")
41-
// createdAtStart()
42-
// }
7+
import radio.data.api.RadioBrowserApi
8+
import radio.data.repository.RadioImpl
9+
import radio.domain.repository.RadioRepository
10+
import radio.presentation.viewmodel.RadioViewModel
11+
12+
val sharedModule = module {
13+
single<PlayerRepository> { PlayerImpl() }
14+
single { RadioBrowserApi() }
15+
single<RadioRepository> { RadioImpl(get()) }
16+
single { PlayerViewModel(get()) }
17+
single { RadioViewModel(get(), get()) }
4318
}
Lines changed: 135 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,149 @@
11
package main.presentation
22

3+
import androidx.compose.animation.*
34
import androidx.compose.foundation.layout.*
45
import androidx.compose.material.*
56
import androidx.compose.runtime.*
67
import androidx.compose.ui.Modifier
7-
import org.koin.java.KoinJavaComponent.getKoin
8-
import player.presentation.compose.SetupPlayer
9-
import player.presentation.viewmodel.PlayerViewModel
10-
import player.util.PlayerState
8+
import androidx.compose.ui.graphics.Color
9+
import androidx.compose.ui.text.font.FontFamily
10+
import androidx.compose.ui.text.font.FontWeight
11+
import androidx.compose.ui.unit.dp
12+
import androidx.compose.ui.unit.sp
13+
import org.koin.compose.koinInject
1114
import radio.presentation.Page.RadioPage
15+
import player.presentation.viewmodel.PlayerViewModel
16+
import player.presentation.compose.SetupPlayer
17+
import radio.presentation.viewmodel.RadioViewModel
1218

1319
@Composable
14-
fun App(player: PlayerViewModel = getKoin().get()) {
15-
MaterialTheme() {
16-
17-
18-
19-
20-
val playerState by player.playerState.collectAsState()
21-
22-
23-
24-
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
25-
when (playerState) {
26-
27-
is PlayerState.Empty -> {
28-
29-
SetupPlayer()
30-
31-
}
32-
33-
else -> {
34-
RadioPage()
35-
20+
fun App() {
21+
val radioViewModel: RadioViewModel = koinInject()
22+
val playerViewModel: PlayerViewModel = koinInject()
23+
val playerState by playerViewModel.playerState.collectAsState()
24+
25+
MaterialTheme(
26+
colors = MaterialTheme.colors.copy(
27+
primary = Color(0xFF2196F3),
28+
secondary = Color(0xFF03DAC6),
29+
background = Color(0xFF121212),
30+
surface = Color(0xFF1E1E1E),
31+
onPrimary = Color.White,
32+
onSecondary = Color.Black,
33+
onBackground = Color.White,
34+
onSurface = Color.White,
35+
onError = Color.White,
36+
error = Color(0xFFCF6679)
37+
),
38+
typography = MaterialTheme.typography.copy(
39+
body1 = MaterialTheme.typography.body1.copy(
40+
fontFamily = FontFamily.Default,
41+
fontSize = 16.sp,
42+
fontWeight = FontWeight.Normal,
43+
color = Color.White
44+
),
45+
button = MaterialTheme.typography.button.copy(
46+
fontFamily = FontFamily.Default,
47+
fontSize = 14.sp,
48+
fontWeight = FontWeight.Medium,
49+
color = Color.White
50+
),
51+
h1 = MaterialTheme.typography.h1.copy(
52+
fontFamily = FontFamily.Default,
53+
fontSize = 32.sp,
54+
fontWeight = FontWeight.Bold,
55+
color = Color.White
56+
),
57+
h2 = MaterialTheme.typography.h2.copy(
58+
fontFamily = FontFamily.Default,
59+
fontSize = 24.sp,
60+
fontWeight = FontWeight.Bold,
61+
color = Color.White
62+
),
63+
h3 = MaterialTheme.typography.h3.copy(
64+
fontFamily = FontFamily.Default,
65+
fontSize = 20.sp,
66+
fontWeight = FontWeight.Bold,
67+
color = Color.White
68+
),
69+
h4 = MaterialTheme.typography.h4.copy(
70+
fontFamily = FontFamily.Default,
71+
fontSize = 18.sp,
72+
fontWeight = FontWeight.Bold,
73+
color = Color.White
74+
),
75+
h5 = MaterialTheme.typography.h5.copy(
76+
fontFamily = FontFamily.Default,
77+
fontSize = 16.sp,
78+
fontWeight = FontWeight.Bold,
79+
color = Color.White
80+
),
81+
h6 = MaterialTheme.typography.h6.copy(
82+
fontFamily = FontFamily.Default,
83+
fontSize = 14.sp,
84+
fontWeight = FontWeight.Bold,
85+
color = Color.White
86+
),
87+
subtitle1 = MaterialTheme.typography.subtitle1.copy(
88+
fontFamily = FontFamily.Default,
89+
fontSize = 16.sp,
90+
fontWeight = FontWeight.Medium,
91+
color = Color.White
92+
),
93+
subtitle2 = MaterialTheme.typography.subtitle2.copy(
94+
fontFamily = FontFamily.Default,
95+
fontSize = 14.sp,
96+
fontWeight = FontWeight.Medium,
97+
color = Color.White
98+
),
99+
caption = MaterialTheme.typography.caption.copy(
100+
fontFamily = FontFamily.Default,
101+
fontSize = 12.sp,
102+
fontWeight = FontWeight.Normal,
103+
color = Color.White
104+
),
105+
overline = MaterialTheme.typography.overline.copy(
106+
fontFamily = FontFamily.Default,
107+
fontSize = 10.sp,
108+
fontWeight = FontWeight.Normal,
109+
color = Color.White
110+
)
111+
)
112+
) {
113+
Surface(
114+
modifier = Modifier.fillMaxSize(),
115+
color = MaterialTheme.colors.background
116+
) {
117+
AnimatedVisibility(
118+
visible = true,
119+
enter = fadeIn() + expandVertically(),
120+
exit = fadeOut() + shrinkVertically()
121+
) {
122+
if (playerState == null) {
123+
Card(
124+
modifier = Modifier
125+
.fillMaxSize()
126+
.padding(16.dp),
127+
elevation = 8.dp,
128+
backgroundColor = MaterialTheme.colors.surface
129+
) {
130+
SetupPlayer(playerViewModel)
131+
}
132+
} else {
133+
Card(
134+
modifier = Modifier
135+
.fillMaxSize()
136+
.padding(16.dp),
137+
elevation = 8.dp,
138+
backgroundColor = MaterialTheme.colors.surface
139+
) {
140+
RadioPage(
141+
viewModel = radioViewModel,
142+
playerViewModel = playerViewModel
143+
)
144+
}
36145
}
37146
}
38147
}
39-
40-
41148
}
42149
}

composeApp/src/desktopMain/kotlin/main/presentation/viewmodel/SharedViewModel.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
package player.presentation.viewmodel
22

3-
import de.sfuhrm.radiobrowser4j.Station
4-
import kotlinx.coroutines.*
5-
import kotlinx.coroutines.flow.MutableStateFlow
6-
import kotlinx.coroutines.flow.StateFlow
7-
import kotlinx.coroutines.flow.collectLatest
8-
import kotlinx.coroutines.flow.flow
93
import org.koin.java.KoinJavaComponent
104
import player.domain.repository.PlayerRepository
11-
import player.util.PlayerReadyState
12-
import player.util.PlayerState
135

146
class SharedViewModel(private val player: PlayerRepository = KoinJavaComponent.getKoin().get()) {
157

0 commit comments

Comments
 (0)