diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index d358539..ce50a5f 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: - java-version: 11 + java-version: 17 distribution: 'zulu' cache: 'gradle' @@ -42,10 +42,10 @@ jobs: keyStorePassword: ${{ secrets.KEYSTOREPASSWORD }} keyPassword: ${{ secrets.KEYPASSWORD }} env: - BUILD_TOOLS_VERSION: "33.0.0" + BUILD_TOOLS_VERSION: "34.0.0" - name: Upload the APK uses: actions/upload-artifact@v2 with: name: uwuradio - path: ${{steps.signing.outputs.signedReleaseFile}} \ No newline at end of file + path: ${{steps.signing.outputs.signedReleaseFile}} diff --git a/clients/android/app/build.gradle.kts b/clients/android/app/build.gradle.kts index 16ba821..d0c37b7 100644 --- a/clients/android/app/build.gradle.kts +++ b/clients/android/app/build.gradle.kts @@ -1,21 +1,20 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id("com.android.application") kotlin("android") kotlin("plugin.serialization") + kotlin("plugin.compose") } android { namespace = "network.uwu.radio" - compileSdk = 33 + compileSdk = 34 defaultConfig { applicationId = "network.uwu.radio" minSdk = 21 - targetSdk = 33 - versionCode = 100 - versionName = "1.0.0" + targetSdk = 34 + versionCode = 200 + versionName = "2.0.0" vectorDrawables { useSupportLibrary = true @@ -25,10 +24,7 @@ android { buildTypes { release { isMinifyEnabled = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } @@ -43,45 +39,31 @@ android { buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.0" } - packagingOptions { + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } -tasks.withType { - kotlinOptions { - freeCompilerArgs += listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + - project.buildDir.absolutePath + "/compose_compiler" - ) -// freeCompilerArgs += listOf( -// "-P", -// "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + -// project.buildDir.absolutePath + "/compose_compiler" -// ) - } -} - dependencies { - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") - implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") + implementation("androidx.activity:activity-compose:1.9.0") - implementation("androidx.compose.foundation:foundation:1.3.1") - implementation("androidx.compose.material3:material3:1.0.1") - debugImplementation("androidx.compose.ui:ui-tooling:1.3.3") - debugImplementation("androidx.compose.ui:ui-test-manifest:1.3.3") + implementation("androidx.compose.foundation:foundation:1.6.7") + debugImplementation("androidx.compose.ui:ui-tooling:1.6.7") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.7") - val media3Version = "1.0.0-beta03" + val media3Version = "1.3.1" implementation("androidx.media3:media3-exoplayer:$media3Version") implementation("androidx.media3:media3-session:$media3Version") @@ -92,11 +74,9 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") - implementation("com.google.accompanist:accompanist-systemuicontroller:0.28.0") - - implementation("io.coil-kt:coil-compose:2.2.2") + implementation("io.coil-kt:coil-compose:2.4.0") - implementation("io.insert-koin:koin-androidx-compose:3.4.1") + implementation("io.insert-koin:koin-androidx-compose:3.4.5") implementation("com.microsoft.signalr:signalr:7.0.0") diff --git a/clients/android/app/proguard-rules.pro b/clients/android/app/proguard-rules.pro index 4922e9a..37d3f1f 100644 --- a/clients/android/app/proguard-rules.pro +++ b/clients/android/app/proguard-rules.pro @@ -34,4 +34,6 @@ # Microsoft SignalR Java API sucks -keep class network.uwu.radio.network.dto.ApiSong { *; } -keep class com.microsoft.signalr.** { *; } --keep interface com.microsoft.signalr.** { *; } \ No newline at end of file +-keep interface com.microsoft.signalr.** { *; } + +-dontwarn org.slf4j.impl.StaticLoggerBinder diff --git a/clients/android/app/src/androidTest/java/network/uwu/radio/ExampleInstrumentedTest.kt b/clients/android/app/src/androidTest/java/network/uwu/radio/ExampleInstrumentedTest.kt deleted file mode 100644 index ef318a3..0000000 --- a/clients/android/app/src/androidTest/java/network/uwu/radio/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package network.uwu.radio - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("network.uwu.radio", appContext.packageName) - } -} diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml index 6638b9a..402474a 100644 --- a/clients/android/app/src/main/AndroidManifest.xml +++ b/clients/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -23,7 +24,7 @@ diff --git a/clients/android/app/src/main/java/network/uwu/radio/MainActivity.kt b/clients/android/app/src/main/java/network/uwu/radio/MainActivity.kt deleted file mode 100644 index ca26edd..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/MainActivity.kt +++ /dev/null @@ -1,35 +0,0 @@ -package network.uwu.radio - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.Modifier -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import network.uwu.radio.ui.screen.HomeScreen -import network.uwu.radio.ui.theme.UwuRadioTheme - -class MainActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - UwuRadioTheme { - val backgroundColor = UwuRadioTheme.colorScheme.background - Box(modifier = Modifier.background(backgroundColor)) { - val systemUiController = rememberSystemUiController() - SideEffect { - systemUiController.setSystemBarsColor( - color = backgroundColor, - darkIcons = false - ) - } - HomeScreen() - } - } - } - } -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/UwuRadio.kt b/clients/android/app/src/main/java/network/uwu/radio/UwuRadio.kt index 809cafa..50cbdc9 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/UwuRadio.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/UwuRadio.kt @@ -9,14 +9,13 @@ import org.koin.core.context.startKoin class UwuRadio : Application() { - override fun onCreate() { - super.onCreate() + override fun onCreate() { + super.onCreate() - startKoin { - androidContext(this@UwuRadio) - - modules(clientModule, mainModule, homeModule) - } - } + startKoin { + androidContext(this@UwuRadio) + modules(clientModule, mainModule, homeModule) + } + } } diff --git a/clients/android/app/src/main/java/network/uwu/radio/di/Client.kt b/clients/android/app/src/main/java/network/uwu/radio/di/Client.kt index 1416fe9..880373d 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/di/Client.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/di/Client.kt @@ -1,39 +1,39 @@ package network.uwu.radio.di import com.microsoft.signalr.HubConnectionBuilder -import io.ktor.client.* -import io.ktor.client.engine.android.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.ANDROID +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import network.uwu.radio.BuildConfig -import network.uwu.radio.network.service.UwuRadioApiService -import network.uwu.radio.network.service.UwuRadioApiServiceImpl -import network.uwu.radio.network.service.UwuRadioSyncService -import network.uwu.radio.network.service.UwuRadioSyncServiceImpl +import network.uwu.radio.network.UwuRadioApiService +import network.uwu.radio.network.UwuRadioSyncService import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind import org.koin.dsl.module val clientModule = module { - single { - HubConnectionBuilder.create("https://radio.k6.tf/sync") - .build() - } - single { - HttpClient(Android) { - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - if (BuildConfig.DEBUG) { - install(Logging) { - logger = Logger.ANDROID - level = LogLevel.ALL - } - } - } - } - singleOf(::UwuRadioSyncServiceImpl) bind UwuRadioSyncService::class - singleOf(::UwuRadioApiServiceImpl) bind UwuRadioApiService::class + single { + HubConnectionBuilder.create("https://radio.k6.tf/sync") + .build() + } + single { + HttpClient(Android) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + if (BuildConfig.DEBUG) { + install(Logging) { + logger = Logger.ANDROID + level = LogLevel.ALL + } + } + } + } + singleOf(::UwuRadioSyncService) + singleOf(::UwuRadioApiService) } diff --git a/clients/android/app/src/main/java/network/uwu/radio/di/Home.kt b/clients/android/app/src/main/java/network/uwu/radio/di/Home.kt index 908aad4..ce3d005 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/di/Home.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/di/Home.kt @@ -1,9 +1,9 @@ package network.uwu.radio.di -import network.uwu.radio.ui.viewmodel.HomeViewModel +import network.uwu.radio.ui.screen.home.HomeViewModel import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module val homeModule = module { - viewModelOf(::HomeViewModel) + viewModelOf(::HomeViewModel) } diff --git a/clients/android/app/src/main/java/network/uwu/radio/di/Main.kt b/clients/android/app/src/main/java/network/uwu/radio/di/Main.kt index 97bb5a3..c07716f 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/di/Main.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/di/Main.kt @@ -1,11 +1,9 @@ package network.uwu.radio.di -import network.uwu.radio.domain.repository.SessionRepository -import network.uwu.radio.domain.repository.SessionRepositoryImpl +import network.uwu.radio.domain.SessionRepository import org.koin.core.module.dsl.singleOf -import org.koin.dsl.bind import org.koin.dsl.module val mainModule = module { - singleOf(::SessionRepositoryImpl) bind SessionRepository::class + singleOf(::SessionRepository) } diff --git a/clients/android/app/src/main/java/network/uwu/radio/domain/SessionRepository.kt b/clients/android/app/src/main/java/network/uwu/radio/domain/SessionRepository.kt new file mode 100644 index 0000000..5355d79 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/domain/SessionRepository.kt @@ -0,0 +1,57 @@ +package network.uwu.radio.domain + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import network.uwu.radio.domain.model.DomainSongData +import network.uwu.radio.network.UwuRadioApiService +import network.uwu.radio.network.UwuRadioSyncService +import network.uwu.radio.network.dto.ApiSong + +class SessionRepository( + private val uwuRadioSyncService: UwuRadioSyncService, + private val uwuRadioApiService: UwuRadioApiService +) { + + suspend fun connectSyncClient() { + uwuRadioSyncService.connect() + } + + suspend fun requestData() { + uwuRadioSyncService.requestState() + } + + fun observeSongData(): Flow> { + return uwuRadioSyncService.observeReceiveState() + .map { + it.currentSong.toDomain( + startTime = it.currentStarted + ) to it.nextSong.toDomain() + } + } + + fun observeNextSong(): Flow { + return uwuRadioSyncService.observeNextBroadcast() + .map { + it.song.toDomain() + } + } + + suspend fun getRandomQuote(submitter: String): String? { + return uwuRadioApiService.getSubmitters() + .find { it.name == submitter } + ?.quotes + ?.randomOrNull() + } + + private suspend fun ApiSong.toDomain(startTime: Long = 0L): DomainSongData { + return DomainSongData( + songName = name, + songArtist = artist, + songArtworkUrl = artUrl, + songUrl = dlUrl!!, + submitterName = submitter, + submitterQuote = getRandomQuote(submitter), + startTime = startTime + ) + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/domain/model/Song.kt b/clients/android/app/src/main/java/network/uwu/radio/domain/model/Song.kt index 8950da0..f3c51a1 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/domain/model/Song.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/domain/model/Song.kt @@ -1,11 +1,11 @@ package network.uwu.radio.domain.model data class DomainSongData( - val songName: String, - val songArtist: String, - val songUrl: String, - val songArtworkUrl: String?, - val submitterName: String, - val submitterQuote: String?, - val startTime: Long + val songName: String, + val songArtist: String, + val songUrl: String, + val songArtworkUrl: String?, + val submitterName: String, + val submitterQuote: String?, + val startTime: Long ) diff --git a/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt b/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt deleted file mode 100644 index bf3edd4..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/domain/repository/SessionRepository.kt +++ /dev/null @@ -1,82 +0,0 @@ -package network.uwu.radio.domain.repository - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import network.uwu.radio.domain.model.DomainSongData -import network.uwu.radio.network.dto.ApiSong -import network.uwu.radio.network.service.ClientState -import network.uwu.radio.network.service.UwuRadioApiService -import network.uwu.radio.network.service.UwuRadioSyncService - -interface SessionRepository { - - suspend fun connectSyncClient() - - suspend fun requestData() - - suspend fun getRandomQuote(submitter: String): String? - - fun observeSyncClientState(): Flow - - fun observeProgressbar(): Flow - fun observeSongData(): Flow> - fun observeNextSong(): Flow - -} - -class SessionRepositoryImpl( - private val uwuRadioSyncService: UwuRadioSyncService, - private val uwuRadioApiService: UwuRadioApiService -) : SessionRepository { - - override suspend fun connectSyncClient() { - uwuRadioSyncService.connect() - } - - override suspend fun requestData() { - uwuRadioSyncService.requestState() - } - - override fun observeSyncClientState(): Flow { - return uwuRadioSyncService.observeClientState() - } - - override fun observeProgressbar(): Flow { - return uwuRadioSyncService.observeSeekPosition() - } - - override fun observeSongData(): Flow> { - return uwuRadioSyncService.observeReceiveState() - .map { - it.currentSong.toDomain( - startTime = it.currentStarted - ) to it.nextSong.toDomain() - } - } - - override fun observeNextSong(): Flow { - return uwuRadioSyncService.observeNextBroadcast() - .map { - it.song.toDomain() - } - } - - override suspend fun getRandomQuote(submitter: String): String? { - return uwuRadioApiService.getSubmitters() - .find { it.name == submitter } - ?.quotes - ?.randomOrNull() - } - - private suspend fun ApiSong.toDomain(startTime: Long = 0L): DomainSongData { - return DomainSongData( - songName = name, - songArtist = artist, - songArtworkUrl = artUrl, - songUrl = dlUrl!!, - submitterName = submitter, - submitterQuote = getRandomQuote(submitter), - startTime = startTime - ) - } -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioApiService.kt b/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioApiService.kt new file mode 100644 index 0000000..3faeda5 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioApiService.kt @@ -0,0 +1,34 @@ +package network.uwu.radio.network + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import network.uwu.radio.network.dto.ApiSubmitter +import network.uwu.radio.network.dto.ApiSubmitters + +class UwuRadioApiService( + private val client: HttpClient +) { + + private val submitters = mutableListOf() + + suspend fun getSubmitters(): List { + return withContext(Dispatchers.IO) { + if (submitters.isEmpty()) { + val apiSubmitters = client.get(getDataUrl()).body().submitters + submitters.addAll(apiSubmitters) + } + submitters + } + } + + companion object { + private const val BASE_URL = "https://radio.k6.tf/api" + + fun getDataUrl(): String { + return "$BASE_URL/data" + } + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioSyncService.kt b/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioSyncService.kt new file mode 100644 index 0000000..bd86b14 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/UwuRadioSyncService.kt @@ -0,0 +1,129 @@ +package network.uwu.radio.network + +import com.microsoft.signalr.HubConnection +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import network.uwu.radio.network.dto.ApiNextBroadcast +import network.uwu.radio.network.dto.ApiReceiveState +import network.uwu.radio.network.dto.ApiSong +import kotlin.coroutines.resume + +class UwuRadioSyncService( + private val hubConnection: HubConnection +) { + + sealed interface ClientState { + data object Disconnected : ClientState + data object Connected : ClientState + } + + private val _clientState = MutableStateFlow(ClientState.Disconnected) + val clientState = _clientState.asStateFlow() + + suspend fun connect() { + suspendCancellableCoroutine { continuation -> + val disposable = hubConnection.start() + .subscribe( + { + _clientState.value = ClientState.Connected + continuation.resume(Unit) + }, + { + _clientState.value = ClientState.Disconnected + continuation.cancel(it) + } + ) + continuation.invokeOnCancellation { + _clientState.tryEmit(ClientState.Disconnected) + hubConnection.close() + disposable.dispose() + } + } + } + + suspend fun requestState() { + suspendCancellableCoroutine { continuation -> + val disposable = hubConnection.invoke("RequestState") + .subscribe( + { + continuation.resume(Unit) + }, + { + continuation.cancel(it) + } + ) + continuation.invokeOnCancellation { + disposable.dispose() + } + } + } + + suspend fun requestSeekPosition() { + suspendCancellableCoroutine { continuation -> + val disposable = hubConnection.invoke("RequestSeekPos") + .subscribe( + { + continuation.resume(Unit) + }, + { + continuation.cancel(it) + } + ) + continuation.invokeOnCancellation { + disposable.dispose() + } + } + } + + fun observeNextBroadcast(): Flow { + return callbackFlow { + val subscription = hubConnection.on("BroadcastNext", { song, startTime -> + trySend(ApiNextBroadcast(song, startTime)) + }, ApiSong::class.java, Long::class.java) + + awaitClose { + subscription.unsubscribe() + } + } + } + + fun observeReceiveState(): Flow { + return callbackFlow { + val subscription = hubConnection.on( + "ReceiveState", + { currentSong, currentStartTime, nextSong, nextStartTime -> + trySend( + ApiReceiveState( + currentSong, + currentStartTime, + nextSong, + nextStartTime + ) + ) + }, + ApiSong::class.java, Long::class.java, ApiSong::class.java, Long::class.java + ) + + awaitClose { + subscription.unsubscribe() + } + } + } + + fun observeSeekPosition(): Flow { + return callbackFlow { + val subscription = hubConnection.on("ReceiveSeekPos", { currentStarted -> + trySend(currentStarted) + }, Long::class.java) + + awaitClose { + subscription.unsubscribe() + } + } + } + +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiNextBroadcast.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiNextBroadcast.kt new file mode 100644 index 0000000..b0f3a82 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiNextBroadcast.kt @@ -0,0 +1,6 @@ +package network.uwu.radio.network.dto + +data class ApiNextBroadcast( + val song: ApiSong, + val startTime: Long +) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiReceiveState.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiReceiveState.kt new file mode 100644 index 0000000..dba015e --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiReceiveState.kt @@ -0,0 +1,8 @@ +package network.uwu.radio.network.dto + +data class ApiReceiveState( + val currentSong: ApiSong, + val currentStarted: Long, + val nextSong: ApiSong, + val nextStart: Long, +) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSong.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSong.kt new file mode 100644 index 0000000..5c71dda --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSong.kt @@ -0,0 +1,11 @@ +package network.uwu.radio.network.dto + +data class ApiSong( + val name: String, + val artist: String, + val dlUrl: String?, + val sourceUrl: String?, + val artUrl: String?, + val album: String?, + val submitter: String, +) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSubmitters.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSubmitters.kt new file mode 100644 index 0000000..a94124d --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/network/dto/ApiSubmitters.kt @@ -0,0 +1,22 @@ +package network.uwu.radio.network.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ApiSubmitters( + @SerialName("submitters") + val submitters: List +) + +@Serializable +data class ApiSubmitter( + @SerialName("name") + val name: String, + + @SerialName("pfpUrl") + val pfpUrl: String, + + @SerialName("quotes") + val quotes: List, +) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/Song.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/Song.kt deleted file mode 100644 index 7e33f80..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/network/dto/Song.kt +++ /dev/null @@ -1,11 +0,0 @@ -package network.uwu.radio.network.dto - -data class ApiSong( - val name: String, - val artist: String, - val dlUrl: String?, - val sourceUrl: String?, - val artUrl: String?, - val album: String?, - val submitter: String, -) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/dto/Submitters.kt b/clients/android/app/src/main/java/network/uwu/radio/network/dto/Submitters.kt deleted file mode 100644 index 9e35d8e..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/network/dto/Submitters.kt +++ /dev/null @@ -1,22 +0,0 @@ -package network.uwu.radio.network.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ApiSubmitters( - @SerialName("submitters") - val submitters: List -) - -@Serializable -data class ApiSubmitter( - @SerialName("name") - val name: String, - - @SerialName("pfpUrl") - val pfpUrl: String, - - @SerialName("quotes") - val quotes: List, -) diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioApiService.kt b/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioApiService.kt deleted file mode 100644 index b91ceb9..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioApiService.kt +++ /dev/null @@ -1,40 +0,0 @@ -package network.uwu.radio.network.service - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import network.uwu.radio.network.dto.ApiSubmitter -import network.uwu.radio.network.dto.ApiSubmitters - -interface UwuRadioApiService { - - suspend fun getSubmitters(): List - -} - -class UwuRadioApiServiceImpl( - private val client: HttpClient -) : UwuRadioApiService { - - private val submitters = mutableListOf() - - override suspend fun getSubmitters(): List { - return withContext(Dispatchers.IO) { - submitters.ifEmpty { - val apiSubmitters = client.get(getDataUrl()).body().submitters - submitters.addAll(apiSubmitters) - apiSubmitters - } - } - } - - companion object { - private const val BASE_URL = "https://radio.k6.tf/api" - - fun getDataUrl(): String { - return "$BASE_URL/data" - } - } -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioSyncService.kt b/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioSyncService.kt deleted file mode 100644 index 52b85bf..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/network/service/UwuRadioSyncService.kt +++ /dev/null @@ -1,155 +0,0 @@ -package network.uwu.radio.network.service - -import com.microsoft.signalr.HubConnection -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import network.uwu.radio.network.dto.ApiSong -import kotlin.coroutines.resume - -data class NextBroadcastData( - val song: ApiSong, - val startTime: Long -) - -data class ReceiveStateData( - val currentSong: ApiSong, - val currentStarted: Long, - val nextSong: ApiSong, - val nextStart: Long, -) - -sealed interface ClientState { - object Disconnected : ClientState - object Connected : ClientState -} - -interface UwuRadioSyncService { - - suspend fun connect() - suspend fun requestState() - suspend fun requestSeekPosition() - - fun observeClientState(): Flow - - fun observeNextBroadcast(): Flow - fun observeReceiveState(): Flow - fun observeSeekPosition(): Flow - -} - -class UwuRadioSyncServiceImpl( - private val hubConnection: HubConnection -) : UwuRadioSyncService { - - private val clientState = MutableSharedFlow(replay = 1) - - override suspend fun connect() { - suspendCancellableCoroutine { continuation -> - val disposable = hubConnection.start() - .subscribe( - { - clientState.tryEmit(ClientState.Connected) - continuation.resume(Unit) - }, - { - clientState.tryEmit(ClientState.Disconnected) - continuation.cancel(it) - } - ) - continuation.invokeOnCancellation { - clientState.tryEmit(ClientState.Disconnected) - hubConnection.close() - disposable.dispose() - } - } - } - - override suspend fun requestState() { - suspendCancellableCoroutine { continuation -> - val disposable = hubConnection.invoke("RequestState") - .subscribe( - { - continuation.resume(Unit) - }, - { - continuation.cancel(it) - } - ) - continuation.invokeOnCancellation { - disposable.dispose() - } - } - } - - override suspend fun requestSeekPosition() { - suspendCancellableCoroutine { continuation -> - val disposable = hubConnection.invoke("RequestSeekPos") - .subscribe( - { - continuation.resume(Unit) - }, - { - continuation.cancel(it) - } - ) - continuation.invokeOnCancellation { - disposable.dispose() - } - } - } - - override fun observeClientState(): Flow { - return clientState - } - - override fun observeNextBroadcast(): Flow { - return callbackFlow { - val subscription = hubConnection.on("BroadcastNext", { song, startTime -> - trySend(NextBroadcastData(song, startTime)) - }, ApiSong::class.java, Long::class.java) - - awaitClose { - subscription.unsubscribe() - } - } - } - - override fun observeReceiveState(): Flow { - return callbackFlow { - val subscription = hubConnection.on( - "ReceiveState", - { currentSong, currentStartTime, nextSong, nextStartTime -> - trySend( - ReceiveStateData( - currentSong, - currentStartTime, - nextSong, - nextStartTime - ) - ) - }, - ApiSong::class.java, Long::class.java, ApiSong::class.java, Long::class.java - ) - - awaitClose { - subscription.unsubscribe() - } - } - } - - override fun observeSeekPosition(): Flow { - return callbackFlow { - val subscription = hubConnection.on("ReceiveSeekPos", { currentStarted -> - trySend(currentStarted) - }, Long::class.java) - - awaitClose { - subscription.unsubscribe() - } - } - } - -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/service/AudioPlaybackService.kt b/clients/android/app/src/main/java/network/uwu/radio/service/AudioPlaybackService.kt index 827c052..6c41200 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/service/AudioPlaybackService.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/service/AudioPlaybackService.kt @@ -1,110 +1,113 @@ package network.uwu.radio.service import android.net.Uri -import android.os.Bundle +import androidx.core.os.bundleOf import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.session.* -import kotlinx.coroutines.* +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionCommands +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import network.uwu.radio.domain.SessionRepository import network.uwu.radio.domain.model.DomainSongData -import network.uwu.radio.domain.repository.SessionRepository import org.koin.android.ext.android.inject class AudioPlaybackService : MediaSessionService() { - private val repository: SessionRepository by inject() + private val repository: SessionRepository by inject() - private lateinit var player: Player - private lateinit var mediaSession: MediaSession + private lateinit var mediaSession: MediaSession - private val job = SupervisorJob() - private val coroutineScope = CoroutineScope(Dispatchers.IO + job) + private val job = SupervisorJob() + private val coroutineScope = CoroutineScope(Dispatchers.IO + job) - override fun onCreate() { - super.onCreate() - player = ExoPlayer.Builder(this).build() - mediaSession = MediaSession - .Builder(this, player) - .setCallback(MediaSessionCallback()) - .build() + override fun onCreate() { + super.onCreate() + val player = ExoPlayer.Builder(this).build() + mediaSession = MediaSession + .Builder(this, player) + .setCallback(MediaSessionCallback()) + .build() - coroutineScope.launch { - repository.observeSongData() - .onEach { - withContext(Dispatchers.Main) { - player.addMediaItem(it.first.toMediaItem()) - player.seekTo( - 0, - (((System.currentTimeMillis() / 1000) - it.first.startTime) * 1000) - ) - player.prepare() - player.play() - player.addMediaItem(it.second.toMediaItem()) - } - } - .launchIn(this) + coroutineScope.launch { + repository.observeSongData() + .onEach { + withContext(Dispatchers.Main) { + player.addMediaItem(it.first.toMediaItem()) + player.seekTo( + 0, + (((System.currentTimeMillis() / 1000) - it.first.startTime) * 1000) + ) + player.prepare() + player.play() + player.addMediaItem(it.second.toMediaItem()) + } + } + .launchIn(this) - repository.observeNextSong() - .onEach { - withContext(Dispatchers.Main) { - if (!player.hasNextMediaItem()) { - player.addMediaItem(it.toMediaItem()) - } - } - } - .launchIn(this) + repository.observeNextSong() + .onEach { + withContext(Dispatchers.Main) { + if (!player.hasNextMediaItem()) { + player.addMediaItem(it.toMediaItem()) + } + } + } + .launchIn(this) - repository.connectSyncClient() - repository.requestData() - } - } + repository.connectSyncClient() + repository.requestData() + } + } - override fun onGetSession( - controllerInfo: MediaSession.ControllerInfo - ): MediaSession = mediaSession + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession - override fun onDestroy() { - job.cancel() - player.release() - mediaSession.release() - super.onDestroy() - } + override fun onDestroy() { + job.cancel() + mediaSession.player.release() + mediaSession.release() + super.onDestroy() + } - private fun DomainSongData.toMediaItem(): MediaItem { - return MediaItem.Builder() - .setMediaMetadata( - MediaMetadata.Builder() - .setArtist(songArtist) - .setArtworkUri(songArtworkUrl?.let { Uri.parse(it) }) - .setTitle(songName) - .setExtras(Bundle().apply { - putString("SUBMITTER_NAME", submitterName) - putString("SUBMITTER_QUOTE", submitterQuote) - putLong("START_TIME", startTime) - }) - .build() - ) - .setUri(Uri.parse(songUrl)) - .build() - } + private fun DomainSongData.toMediaItem(): MediaItem { + val mediaMetadata = MediaMetadata.Builder() + .setArtist(songArtist) + .setArtworkUri(songArtworkUrl?.let { Uri.parse(it) }) + .setTitle(songName) + .setExtras( + bundleOf( + "SUBMITTER_NAME" to submitterName, + "SUBMITTER_QUOTE" to submitterQuote, + "START_TIME" to startTime + ) + ) + .build() - //TODO adjust the button layout somehow - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - inner class MediaSessionCallback : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): MediaSession.ConnectionResult { - return MediaSession.ConnectionResult.accept( - SessionCommands.EMPTY, - Player.Commands.Builder() - .add(Player.COMMAND_GET_TIMELINE) - .build() - ) - } - } + return MediaItem.Builder() + .setMediaMetadata(mediaMetadata) + .setUri(Uri.parse(songUrl)) + .build() + } + + @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) + inner class MediaSessionCallback : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): MediaSession.ConnectionResult { + val playerCommands = Player.Commands.Builder() + .add(Player.COMMAND_GET_TIMELINE) + .add(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build() + return MediaSession.ConnectionResult.accept(SessionCommands.EMPTY, playerCommands) + } + } } diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/MainActivity.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/MainActivity.kt new file mode 100644 index 0000000..4994107 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/MainActivity.kt @@ -0,0 +1,36 @@ +package network.uwu.radio.ui + +import android.graphics.Color +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.ui.Modifier +import network.uwu.radio.ui.screen.home.HomeScreen +import network.uwu.radio.ui.theme.UwuRadioTheme + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT) + ) + super.onCreate(savedInstanceState) + + setContent { + UwuRadioTheme { + val backgroundColor = UwuRadioTheme.colorScheme.background + Box(modifier = Modifier + .systemBarsPadding() + .background(backgroundColor)) { + HomeScreen() + } + } + } + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/component/Divider.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Divider.kt new file mode 100644 index 0000000..897228c --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Divider.kt @@ -0,0 +1,27 @@ +package network.uwu.radio.ui.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import network.uwu.radio.ui.theme.UwuRadioTheme + +@Composable +fun Divider( + modifier: Modifier = Modifier, + thickness: Dp = Dp.Hairline, + color: Color = UwuRadioTheme.colorScheme.onBackground +) { + Canvas( + modifier = modifier.height(thickness) + ) { + drawLine( + color = color, + start = Offset(0f, 0f), + end = Offset(size.width, size.height) + ) + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/component/RemoteImage.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/component/RemoteImage.kt new file mode 100644 index 0000000..de5445e --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/component/RemoteImage.kt @@ -0,0 +1,29 @@ +package network.uwu.radio.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest + +@Composable +fun RemoteImage( + url: String, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val image = remember(context, url) { + ImageRequest.Builder(context) + .data(url) + .diskCachePolicy(CachePolicy.ENABLED) + .diskCacheKey(url) + .build() + } + AsyncImage( + modifier = modifier, + model = image, + contentDescription = null + ) +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/component/Seekbar.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Seekbar.kt index 294a677..11c83ad 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/component/Seekbar.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Seekbar.kt @@ -1,5 +1,6 @@ package network.uwu.radio.ui.component +import androidx.annotation.FloatRange import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,37 +16,37 @@ import androidx.compose.ui.unit.dp import network.uwu.radio.ui.theme.UwuRadioTheme @Composable -fun SeekBar( - progress: Float, - leadingItem: @Composable () -> Unit, - trailingItem: @Composable () -> Unit, - modifier: Modifier = Modifier, - color: Color = UwuRadioTheme.colorScheme.onBackground, +fun ProgressBar( + @FloatRange(from = 0.0, to = 1.0) progress: Float, + leadingItem: @Composable () -> Unit, + trailingItem: @Composable () -> Unit, + modifier: Modifier = Modifier, + color: Color = UwuRadioTheme.colorScheme.onBackground, ) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - CompositionLocalProvider( - LocalTextStyle provides UwuRadioTheme.typography.body, - LocalContentColor provides color - ) { - leadingItem() - trailingItem() - } - } - Canvas(Modifier.fillMaxWidth()) { - drawLine( - color = color, - start = Offset(0f, size.height / 2f), - end = Offset(progress * size.width, size.height / 2f), - strokeWidth = 2f - ) - } - } + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.body, + LocalContentColor provides color + ) { + leadingItem() + trailingItem() + } + } + Canvas(Modifier.fillMaxWidth()) { + drawLine( + color = color, + start = Offset(0f, size.height / 2f), + end = Offset(progress * size.width, size.height / 2f), + strokeWidth = 2f + ) + } + } } diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/component/Text.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Text.kt index 5a922d4..959ccca 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/component/Text.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/component/Text.kt @@ -10,23 +10,23 @@ import androidx.compose.ui.text.style.TextOverflow @Composable fun Text( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = LocalTextStyle.current, - color: Color = LocalContentColor.current, - onTextLayout: (TextLayoutResult) -> Unit = {}, - overflow: TextOverflow = TextOverflow.Clip, - softWrap: Boolean = true, - maxLines: Int = Int.MAX_VALUE, + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + color: Color = LocalContentColor.current, + onTextLayout: (TextLayoutResult) -> Unit = {}, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, ) { - val mergedStyle = style.copy(color = color) - BasicText( - text = text, - modifier = modifier, - style = mergedStyle, - onTextLayout = onTextLayout, - overflow = overflow, - softWrap = softWrap, - maxLines = maxLines - ) + val mergedStyle = style.copy(color = color) + BasicText( + text = text, + modifier = modifier, + style = mergedStyle, + onTextLayout = onTextLayout, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines + ) } diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/screen/Home.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/Home.kt deleted file mode 100644 index 77bb2b7..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/screen/Home.kt +++ /dev/null @@ -1,154 +0,0 @@ -package network.uwu.radio.ui.screen - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.CachePolicy -import coil.request.ImageRequest -import network.uwu.radio.R -import network.uwu.radio.ui.component.SeekBar -import network.uwu.radio.ui.component.Text -import network.uwu.radio.ui.theme.UwuRadioTheme -import network.uwu.radio.ui.viewmodel.HomeViewModel -import org.koin.androidx.compose.koinViewModel - -@Composable -fun HomeScreen() { - val viewModel: HomeViewModel = koinViewModel() - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - contentAlignment = Alignment.Center - ) { - Text(stringResource(R.string.home_title), style = UwuRadioTheme.typography.title) - } - val borderColor = UwuRadioTheme.colorScheme.onBackground - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(Dp.Hairline) - ) { - drawLine( - color = borderColor, - start = Offset(0f, 0f), - end = Offset(size.width, size.height) - ) - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - when (viewModel.loading) { - true -> { - Text( - stringResource(id = R.string.home_loading), - style = UwuRadioTheme.typography.title - ) - } - false -> { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Artwork(viewModel.artUrl) - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.home_song_name, viewModel.name), - style = UwuRadioTheme.typography.title.copy(textAlign = TextAlign.Center) - ) - Text( - text = stringResource( - R.string.home_song_artist, - viewModel.artist - ), - style = UwuRadioTheme.typography.subtitle.copy(textAlign = TextAlign.Center) - ) - Text( - text = stringResource( - R.string.home_song_submission, - viewModel.submitter - ), - style = UwuRadioTheme.typography.label.copy(textAlign = TextAlign.Center) - ) - } - SeekBar( - modifier = Modifier.fillMaxWidth(), - progress = viewModel.progress, - leadingItem = { - Text(viewModel.currentTime) - }, - trailingItem = { - Text(viewModel.totalTime) - } - ) - } - Text( - text = viewModel.quote?.let { - stringResource( - R.string.home_song_quote, - it - ) - } ?: "", - style = UwuRadioTheme.typography.body.copy(textAlign = TextAlign.Center) - ) - } - } - } - } - } -} - -@Composable -private fun Artwork( - url: String?, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val image = remember(context, url) { - ImageRequest.Builder(context) - .data(url) - .diskCachePolicy(CachePolicy.ENABLED) - .diskCacheKey(url) - .build() - } - AsyncImage( - modifier = modifier - .fillMaxWidth() - .aspectRatio(1f / 1f) - .border(Dp.Hairline, UwuRadioTheme.colorScheme.onBackground), - model = image, - contentDescription = null, - ) -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreen.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreen.kt new file mode 100644 index 0000000..603b489 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreen.kt @@ -0,0 +1,223 @@ +package network.uwu.radio.ui.screen.home + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import network.uwu.radio.R +import network.uwu.radio.ui.component.Divider +import network.uwu.radio.ui.component.LocalTextStyle +import network.uwu.radio.ui.component.ProgressBar +import network.uwu.radio.ui.component.RemoteImage +import network.uwu.radio.ui.component.Text +import network.uwu.radio.ui.theme.UwuRadioTheme +import org.koin.androidx.compose.koinViewModel + +@Composable +fun HomeScreen() { + val viewModel: HomeViewModel = koinViewModel() + val state by viewModel.state.collectAsStateWithLifecycle() + val progress by viewModel.progress.collectAsStateWithLifecycle() + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + contentAlignment = Alignment.Center + ) { + Text(stringResource(R.string.home_title), style = UwuRadioTheme.typography.title) + } + Divider(modifier = Modifier.fillMaxWidth()) + } + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + // Kotlin bug, smart casting doesn't work without this + when (val state = state) { + is HomeScreenState.Loading -> { + HomeScreenLayout( + artwork = { + Image( + painter = painterResource(R.drawable.ic_thumbnail), + contentDescription = null + ) + }, + name = { Text(stringResource(R.string.home_loading)) }, + artist = { Text("???") }, + submitter = { Text("???") }, + progress = { + ProgressBar( + modifier = Modifier.fillMaxWidth(), + progress = 1f, + leadingItem = { Text("?:??") }, + trailingItem = { Text("?:??") } + ) + }, + quote = { Text("") } + ) + } + + is HomeScreenState.Success -> { + HomeScreenLayout( + artwork = { + if (state.songArtworkUrl != null) { + RemoteImage(state.songArtworkUrl) + } + }, + name = { Text(stringResource(R.string.home_song_name, state.songName)) }, + artist = { Text(stringResource(R.string.home_song_artist, state.songArtist)) }, + submitter = { Text(stringResource(R.string.home_song_submission, state.submitter)) }, + progress = { + ProgressBar( + modifier = Modifier.fillMaxWidth(), + progress = progress.progress, + leadingItem = { + Text(progress.currentTime.toString()) + }, + trailingItem = { + Text(progress.totalTime.toString()) + } + ) + }, + quote = { + if (state.quote != null) { + Text(stringResource(R.string.home_song_quote, state.quote)) + } else { + Text("") + } + } + ) + } + } + } + } +} + +@Composable +private fun HomeScreenLayout( + modifier: Modifier = Modifier, + artwork: @Composable () -> Unit, + name: @Composable () -> Unit, + artist: @Composable () -> Unit, + submitter: @Composable () -> Unit, + progress: @Composable () -> Unit, + quote: @Composable () -> Unit, +) { + val configuration = LocalConfiguration.current + val orientation = configuration.orientation + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + Row( + modifier = modifier + .heightIn(max = 350.dp) + .widthIn(max = 1000.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(32.dp) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f), + propagateMinConstraints = true + ) { + artwork() + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.title, + content = name + ) + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.subtitle, + content = artist + ) + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.label, + content = submitter + ) + } + progress() + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.body.copy(textAlign = TextAlign.Center), + content = quote + ) + } + } + } else { + Column( + modifier = modifier.widthIn(max = 600.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + propagateMinConstraints = true + ) { + artwork() + } + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.title.copy(textAlign = TextAlign.Center), + content = name + ) + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.subtitle.copy(textAlign = TextAlign.Center), + content = artist + ) + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.label.copy(textAlign = TextAlign.Center), + content = submitter + ) + } + progress() + CompositionLocalProvider( + LocalTextStyle provides UwuRadioTheme.typography.body.copy(textAlign = TextAlign.Center), + content = quote + ) + } + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreenState.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreenState.kt new file mode 100644 index 0000000..d38a1d1 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeScreenState.kt @@ -0,0 +1,31 @@ +package network.uwu.radio.ui.screen.home + +import network.uwu.radio.util.TimeRepresentable + +sealed interface HomeScreenState { + + data object Loading : HomeScreenState + + data class Success( + val songArtworkUrl: String?, + val songName: String, + val songArtist: String, + val submitter: String, + val quote: String?, + ) : HomeScreenState + +} + +data class HomeScreenProgress( + val currentTime: TimeRepresentable, + val totalTime: TimeRepresentable, + val progress: Float +) { + companion object { + val Zero = HomeScreenProgress( + currentTime = TimeRepresentable(0), + totalTime = TimeRepresentable(0), + progress = 0f + ) + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeViewModel.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeViewModel.kt new file mode 100644 index 0000000..31a8897 --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/screen/home/HomeViewModel.kt @@ -0,0 +1,68 @@ +package network.uwu.radio.ui.screen.home + +import android.app.Application +import android.content.ComponentName +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import network.uwu.radio.service.AudioPlaybackService +import network.uwu.radio.util.TimeRepresentable + +class HomeViewModel(application: Application) : AndroidViewModel(application) { + + private val sessionToken = SessionToken( + application, + ComponentName(application, AudioPlaybackService::class.java) + ) + private val mediaControllerFuture = + MediaController.Builder(application, sessionToken).buildAsync() + + private val _state = MutableStateFlow(HomeScreenState.Loading) + val state = _state.asStateFlow() + + private val _progress = MutableStateFlow(HomeScreenProgress.Zero) + val progress = _progress.asStateFlow() + + override fun onCleared() { + super.onCleared() + MediaController.releaseFuture(mediaControllerFuture) + } + + init { + mediaControllerFuture.addListener({ + val mediaController = mediaControllerFuture.get() + mediaController.addListener(object : Player.Listener { + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (mediaItem != null) { + _state.value = HomeScreenState.Success( + songArtworkUrl = mediaItem.mediaMetadata.artworkUri?.toString(), + songName = mediaItem.mediaMetadata.title.toString(), + songArtist = mediaItem.mediaMetadata.artist.toString(), + submitter = mediaItem.mediaMetadata.extras!!.getString("SUBMITTER_NAME")!!, + quote = mediaItem.mediaMetadata.extras!!.getString("SUBMITTER_QUOTE"), + ) + } + } + }) + + viewModelScope.launch { + while (true) { + _progress.value = HomeScreenProgress( + currentTime = TimeRepresentable(mediaController.currentPosition), + totalTime = TimeRepresentable(mediaController.duration), + progress = mediaController.currentPosition.toFloat() / mediaController.duration.toFloat() + ) + delay(1000L) + } + } + }, MoreExecutors.directExecutor()) + } +} diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Color.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Color.kt index 2047404..de28437 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Color.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Color.kt @@ -7,35 +7,35 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color fun colorScheme( - background: Color = Color.Black, - inverseBackground: Color = Color.White, - onBackground: Color = Color.White, - inverseOnBackground: Color = Color.Black + background: Color = Color.Black, + inverseBackground: Color = Color.White, + onBackground: Color = Color.White, + inverseOnBackground: Color = Color.Black ): ColorScheme { - return ColorScheme( - background = background, - inverseBackground = inverseBackground, - onBackground = onBackground, - inverseOnBackground = inverseOnBackground - ) + return ColorScheme( + background = background, + inverseBackground = inverseBackground, + onBackground = onBackground, + inverseOnBackground = inverseOnBackground + ) } @Stable class ColorScheme( - background: Color, - inverseBackground: Color, - onBackground: Color, - inverseOnBackground: Color + background: Color, + inverseBackground: Color, + onBackground: Color, + inverseOnBackground: Color ) { - var background by mutableStateOf(background) - internal set + var background by mutableStateOf(background) + internal set - var inverseBackground by mutableStateOf(inverseBackground) - internal set + var inverseBackground by mutableStateOf(inverseBackground) + internal set - var onBackground by mutableStateOf(onBackground) - internal set + var onBackground by mutableStateOf(onBackground) + internal set - var inverseOnBackground by mutableStateOf(inverseOnBackground) - internal set + var inverseOnBackground by mutableStateOf(inverseOnBackground) + internal set } diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Theme.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Theme.kt index 5c68a5b..032a967 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Theme.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Theme.kt @@ -9,30 +9,30 @@ import network.uwu.radio.ui.component.LocalTextStyle @Composable fun UwuRadioTheme( - colorScheme: ColorScheme = colorScheme(), - typography: Typography = typography(), - content: @Composable () -> Unit + colorScheme: ColorScheme = colorScheme(), + typography: Typography = typography(), + content: @Composable () -> Unit ) { - CompositionLocalProvider( - LocalTypography provides typography, - LocalColorScheme provides colorScheme, - LocalTextStyle provides typography.body, - LocalContentColor provides colorScheme.onBackground, - content = content - ) + CompositionLocalProvider( + LocalTypography provides typography, + LocalColorScheme provides colorScheme, + LocalTextStyle provides typography.body, + LocalContentColor provides colorScheme.onBackground, + content = content + ) } object UwuRadioTheme { - val colorScheme - @Composable - @ReadOnlyComposable - get() = LocalColorScheme.current + val colorScheme + @Composable + @ReadOnlyComposable + get() = LocalColorScheme.current - val typography - @Composable - @ReadOnlyComposable - get() = LocalTypography.current + val typography + @Composable + @ReadOnlyComposable + get() = LocalTypography.current } diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Typography.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Typography.kt index f7fad60..f73e73f 100644 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Typography.kt +++ b/clients/android/app/src/main/java/network/uwu/radio/ui/theme/Typography.kt @@ -9,48 +9,48 @@ import androidx.compose.ui.unit.sp import network.uwu.radio.R private val fontFamily = FontFamily( - Font(R.font.ibmplexmono_extralight, weight = FontWeight.ExtraLight), - Font(R.font.ibmplexmono_light, weight = FontWeight.Light), - Font(R.font.ibmplexmono_regular, weight = FontWeight.Normal), - Font(R.font.ibmplexmono_medium, weight = FontWeight.Medium), - Font(R.font.ibmplexmono_semibold, weight = FontWeight.SemiBold), - Font(R.font.ibmplexmono_bold, weight = FontWeight.Bold), + Font(R.font.ibmplexmono_extralight, weight = FontWeight.ExtraLight), + Font(R.font.ibmplexmono_light, weight = FontWeight.Light), + Font(R.font.ibmplexmono_regular, weight = FontWeight.Normal), + Font(R.font.ibmplexmono_medium, weight = FontWeight.Medium), + Font(R.font.ibmplexmono_semibold, weight = FontWeight.SemiBold), + Font(R.font.ibmplexmono_bold, weight = FontWeight.Bold), ) fun typography( - title: TextStyle = TextStyle( - fontFamily = fontFamily, - fontSize = 20.sp, - lineHeight = 28.sp - ), - subtitle: TextStyle = TextStyle( - fontFamily = fontFamily, - fontSize = 18.sp, - lineHeight = 28.sp - ), - body: TextStyle = TextStyle( - fontFamily = fontFamily, - fontSize = 16.sp, - lineHeight = 24.sp - ), - label: TextStyle = TextStyle( - fontFamily = fontFamily, - fontSize = 14.sp, - lineHeight = 20.sp - ), + title: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 20.sp, + lineHeight = 28.sp + ), + subtitle: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 18.sp, + lineHeight = 28.sp + ), + body: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 16.sp, + lineHeight = 24.sp + ), + label: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 14.sp, + lineHeight = 20.sp + ), ): Typography { - return Typography( - title = title, - subtitle = subtitle, - body = body, - label = label, - ) + return Typography( + title = title, + subtitle = subtitle, + body = body, + label = label, + ) } @Immutable data class Typography( - val title: TextStyle, - val subtitle: TextStyle, - val body: TextStyle, - val label: TextStyle, + val title: TextStyle, + val subtitle: TextStyle, + val body: TextStyle, + val label: TextStyle, ) diff --git a/clients/android/app/src/main/java/network/uwu/radio/ui/viewmodel/HomeViewModel.kt b/clients/android/app/src/main/java/network/uwu/radio/ui/viewmodel/HomeViewModel.kt deleted file mode 100644 index 94fe98e..0000000 --- a/clients/android/app/src/main/java/network/uwu/radio/ui/viewmodel/HomeViewModel.kt +++ /dev/null @@ -1,154 +0,0 @@ -package network.uwu.radio.ui.viewmodel - -import android.app.Application -import android.content.ComponentName -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import androidx.media3.common.MediaItem -import androidx.media3.common.Player -import androidx.media3.session.MediaController -import androidx.media3.session.SessionToken -import com.google.common.util.concurrent.MoreExecutors -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import network.uwu.radio.service.AudioPlaybackService - -class HomeViewModel(application: Application) : AndroidViewModel(application) { - - private val sessionToken = SessionToken( - application, - ComponentName(application, AudioPlaybackService::class.java) - ) - private val mediaControllerFuture = MediaController.Builder(application, sessionToken).buildAsync() - - private var _totalTime: Long = 0 - private var _currentTime: Long = 0 - - var artUrl by mutableStateOf(null) - private set - - var name by mutableStateOf("") - private set - - var artist by mutableStateOf("") - private set - - var submitter by mutableStateOf("") - private set - - var quote by mutableStateOf(null) - private set - - var totalTime by mutableStateOf("") - private set - - var currentTime by mutableStateOf("") - private set - - var progress by mutableStateOf(0f) - private set - - var loading by mutableStateOf(true) - private set - - fun updateArtworkUrl(url: String?) { - artUrl = url - } - - fun updateName(name: String) { - this.name = name - } - - fun updateArtist(artist: String) { - this.artist = artist - } - - fun updateSubmitter(submitter: String) { - this.submitter = submitter - } - - fun updateQuote(quote: String?) { - this.quote = quote - } - - fun updateTotalTime(millis: Long) { - val seconds = millis / 1000 - _totalTime = millis - totalTime = String.format("%01d:%02d", seconds / 60, seconds % 60) - } - - fun updateCurrentTime(millis: Long) { - val seconds = millis / 1000 - _currentTime = millis - currentTime = String.format("%01d:%02d", seconds / 60, seconds % 60) - progress = _currentTime.toFloat() / _totalTime.toFloat() - } - - fun updateLoading(loading: Boolean) { - this.loading = loading - } - - override fun onCleared() { - super.onCleared() - MediaController.releaseFuture(mediaControllerFuture) - } - - init { - mediaControllerFuture.addListener({ - val mediaController = mediaControllerFuture.get() - mediaController.addListener(PlayerListener(mediaController)) - - updateUi(mediaController.currentMediaItem) - updateState(mediaController.playbackState, mediaController.duration) - - viewModelScope.launch { - observeProgress(mediaController) - } - }, MoreExecutors.directExecutor()) - } - - private tailrec suspend fun observeProgress(controller: MediaController) { - if (controller.isPlaying) { - updateCurrentTime(controller.currentPosition) - } - delay(1000L) - observeProgress(controller) - } - - private fun updateUi(mediaItem: MediaItem?) { - if (mediaItem != null) { - updateName(mediaItem.mediaMetadata.title.toString()) - updateArtworkUrl(mediaItem.mediaMetadata.artworkUri?.toString()) - updateArtist(mediaItem.mediaMetadata.artist.toString()) - updateQuote(mediaItem.mediaMetadata.extras!!.getString("SUBMITTER_QUOTE")) - updateSubmitter(mediaItem.mediaMetadata.extras!!.getString("SUBMITTER_NAME")!!) - } - } - - fun updateState(playbackState: Int, totalDuration: Long) { - when (playbackState) { - Player.STATE_READY -> { - updateTotalTime(totalDuration) - updateLoading(false) - } - else -> { - updateLoading(true) - } - } - } - - inner class PlayerListener( - private val mediaController: MediaController - ) : Player.Listener { - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - updateUi(mediaItem) - } - - override fun onPlaybackStateChanged(playbackState: Int) { - updateState(playbackState, mediaController.duration) - } - } -} diff --git a/clients/android/app/src/main/java/network/uwu/radio/util/TimeRepresentable.kt b/clients/android/app/src/main/java/network/uwu/radio/util/TimeRepresentable.kt new file mode 100644 index 0000000..f1e4eaa --- /dev/null +++ b/clients/android/app/src/main/java/network/uwu/radio/util/TimeRepresentable.kt @@ -0,0 +1,12 @@ +package network.uwu.radio.util + +import java.util.Locale + +@JvmInline +value class TimeRepresentable(val millis: Long) { + override fun toString(): String { + val seconds = millis / 1000 + val minutes = seconds / 60 + return String.format(Locale.US, "%01d:%02d", minutes, seconds % 60) + } +} diff --git a/clients/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/clients/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 7706ab9..0000000 --- a/clients/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - diff --git a/clients/android/app/src/main/res/drawable/ic_launcher_background.xml b/clients/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/clients/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/clients/android/app/src/main/res/drawable/ic_thumbnail.webp b/clients/android/app/src/main/res/drawable/ic_thumbnail.webp new file mode 100644 index 0000000..2ec8d38 Binary files /dev/null and b/clients/android/app/src/main/res/drawable/ic_thumbnail.webp differ diff --git a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index ed83961..0000000 Binary files a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c2dc3fd Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index b7ca20d..0000000 Binary files a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 2737839..0000000 Binary files a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..06e9b1d Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 44a1a41..0000000 Binary files a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..df9cae3 Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 41a571b..0000000 Binary files a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index db5f392..0000000 Binary files a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..0a889a8 Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 02c04a1..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..040d49f Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 94b117b..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 2071ba3..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..2d750ba Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 1a97080..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..285fdc3 Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f099a85..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index a2d7052..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..aefc057 Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 69caec5..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..c2d60b7 Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 9222677..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index c83db88..0000000 Binary files a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..ebde6ff Binary files /dev/null and b/clients/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/clients/android/app/src/main/res/values/ic_launcher_background.xml b/clients/android/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index f42ada6..0000000 --- a/clients/android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - diff --git a/clients/android/app/src/test/java/network/uwu/radio/ExampleUnitTest.kt b/clients/android/app/src/test/java/network/uwu/radio/ExampleUnitTest.kt deleted file mode 100644 index d77d081..0000000 --- a/clients/android/app/src/test/java/network/uwu/radio/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package network.uwu.radio - -import org.junit.Assert.* -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/clients/android/build.gradle.kts b/clients/android/build.gradle.kts index 6770110..81845a1 100644 --- a/clients/android/build.gradle.kts +++ b/clients/android/build.gradle.kts @@ -1,6 +1,7 @@ plugins { - id("com.android.application") version "7.4.0" apply false - id("com.android.library") version "7.4.0" apply false - kotlin("android") version "1.8.0" apply false - kotlin("plugin.serialization") version "1.8.0" apply false + id("com.android.application") version "8.4.1" apply false + id("com.android.library") version "8.4.1" apply false + kotlin("android") version "2.0.0" apply false + kotlin("plugin.serialization") version "2.0.0" apply false + kotlin("plugin.compose") version "2.0.0" apply false } diff --git a/clients/android/gradle/wrapper/gradle-wrapper.properties b/clients/android/gradle/wrapper/gradle-wrapper.properties index 1e7d215..00b6748 100644 --- a/clients/android/gradle/wrapper/gradle-wrapper.properties +++ b/clients/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Nov 26 23:47:01 GET 2022 +#Fri Jun 07 12:08:50 GET 2024 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists