mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-06 11:14:00 +00:00
feat(update): implement update checker with dialog for available updates
This commit is contained in:
parent
046c17d3df
commit
3a85ad0896
@ -20,6 +20,7 @@ import gobley.gradle.cargo.dsl.jvm
|
|||||||
import dev.nucleusframework.desktop.application.dsl.TargetFormat
|
import dev.nucleusframework.desktop.application.dsl.TargetFormat
|
||||||
import org.jetbrains.compose.reload.gradle.ComposeHotRun
|
import org.jetbrains.compose.reload.gradle.ComposeHotRun
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import org.gradle.api.tasks.Copy
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
@ -39,11 +40,53 @@ plugins {
|
|||||||
kotlin("plugin.atomicfu") version libs.versions.kotlin
|
kotlin("plugin.atomicfu") version libs.versions.kotlin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bundleId = "dev.krtirtho.spotube"
|
||||||
|
val version = "6.0.0"
|
||||||
|
val buildNumber = 46
|
||||||
|
val targetName = "Spotube"
|
||||||
|
val homePageUrl = "https://spotube.cc"
|
||||||
|
val repositoryUrl = "https://github.com/team-spotube/spotube"
|
||||||
|
val appVersionName = version
|
||||||
|
val appBuildNumber = buildNumber
|
||||||
|
val appRepositoryUrl = repositoryUrl
|
||||||
|
val githubRepositoryPath = repositoryUrl.removePrefix("https://github.com/")
|
||||||
|
val latestReleaseApiUrl = "https://api.github.com/repos/$githubRepositoryPath/releases/latest"
|
||||||
|
val generatedAppInfoDir = layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin")
|
||||||
|
val generateAppBuildInfo by tasks.registering(Copy::class) {
|
||||||
|
description = "Generate AppBuildInfo.kt"
|
||||||
|
from(layout.projectDirectory.file("src/commonMain/templates/AppBuildInfo.kt.template"))
|
||||||
|
into(layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin/dev/krtirtho/spotube"))
|
||||||
|
rename("AppBuildInfo.kt.template", "AppBuildInfo.kt")
|
||||||
|
expand(
|
||||||
|
mapOf(
|
||||||
|
"appVersion" to appVersionName,
|
||||||
|
"appBuildNumber" to appBuildNumber,
|
||||||
|
"repositoryUrl" to appRepositoryUrl,
|
||||||
|
"latestReleaseApiUrl" to latestReleaseApiUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
inputs.properties(
|
||||||
|
mapOf(
|
||||||
|
"appVersion" to appVersionName,
|
||||||
|
"appBuildNumber" to appBuildNumber,
|
||||||
|
"repositoryUrl" to appRepositoryUrl,
|
||||||
|
"latestReleaseApiUrl" to latestReleaseApiUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.configureEach {
|
||||||
|
if (name.startsWith("compile") && name.contains("Kotlin")) {
|
||||||
|
dependsOn(generateAppBuildInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vlcjBundler {
|
vlcjBundler {
|
||||||
packageName = "dev.krtirtho.spotube.core.generated" // choose a different package
|
packageName = "$bundleId.core.generated" // choose a different package
|
||||||
objectName = "VLCBundleLoaderGenerated" // or rename the object
|
objectName = "VLCBundleLoaderGenerated" // or rename the object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
// Note: For Android application modules, androidTarget() is still required as of AGP 8.x.
|
// Note: For Android application modules, androidTarget() is still required as of AGP 8.x.
|
||||||
// The deprecation warning is expected. For libraries, use the androidKotlinMultiplatformLibrary plugin instead.
|
// The deprecation warning is expected. For libraries, use the androidKotlinMultiplatformLibrary plugin instead.
|
||||||
@ -68,6 +111,7 @@ kotlin {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
|
kotlin.srcDir(generatedAppInfoDir)
|
||||||
dependencies {
|
dependencies {
|
||||||
// Project dependencies
|
// Project dependencies
|
||||||
implementation(project(":plugin_interfaces"))
|
implementation(project(":plugin_interfaces"))
|
||||||
@ -172,6 +216,10 @@ kotlin {
|
|||||||
implementation(libs.mqtt.x.models)
|
implementation(libs.mqtt.x.models)
|
||||||
implementation(libs.mqtt.buffer)
|
implementation(libs.mqtt.buffer)
|
||||||
implementation(libs.mqtt.buffer.codec)
|
implementation(libs.mqtt.buffer.codec)
|
||||||
|
|
||||||
|
// Markdown renderer
|
||||||
|
implementation(libs.multiplatform.markdown.renderer)
|
||||||
|
implementation(libs.multiplatform.markdown.renderer.m3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
@ -249,11 +297,11 @@ android {
|
|||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "dev.krtirtho.spotube"
|
applicationId = bundleId
|
||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||||
versionCode = 1
|
versionCode = buildNumber
|
||||||
versionName = project.findProperty("versionName") as String? ?: "1.0"
|
versionName = version
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@ -312,8 +360,8 @@ nucleus.application {
|
|||||||
)
|
)
|
||||||
|
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
packageName = "dev.krtirtho.spotube"
|
packageName = bundleId
|
||||||
packageVersion = project.findProperty("versionName") as String? ?: "6.0.0"
|
packageVersion = version
|
||||||
licenseFile = project.file("../LICENSE")
|
licenseFile = project.file("../LICENSE")
|
||||||
|
|
||||||
appResourcesRootDir.set(vlcjBundler.vlcNativesDirectory)
|
appResourcesRootDir.set(vlcjBundler.vlcNativesDirectory)
|
||||||
@ -329,7 +377,7 @@ nucleus.application {
|
|||||||
|
|
||||||
modules("jdk.unsupported")
|
modules("jdk.unsupported")
|
||||||
|
|
||||||
homepage = "https://spotube.cc"
|
homepage = homePageUrl
|
||||||
|
|
||||||
linux {
|
linux {
|
||||||
modules("jdk.security.auth")
|
modules("jdk.security.auth")
|
||||||
|
|||||||
@ -22,49 +22,54 @@ import androidx.compose.animation.SharedTransitionLayout
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import com.mikepenz.markdown.m3.Markdown
|
||||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES
|
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES
|
||||||
import dev.krtirtho.spotube.core.navigation.rememberNavigationState
|
import dev.krtirtho.spotube.core.navigation.rememberNavigationState
|
||||||
import dev.krtirtho.spotube.core.navigation.toEntries
|
import dev.krtirtho.spotube.core.navigation.toEntries
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.ThemedDialog
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
||||||
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme
|
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||||
import dev.krtirtho.spotube.modules.shell.AppShell
|
import dev.krtirtho.spotube.modules.shell.AppShell
|
||||||
|
import dev.krtirtho.spotube.modules.update.UpdateCheckerViewModel
|
||||||
import dev.krtirtho.spotube.modules.webview.WebViewScreen
|
import dev.krtirtho.spotube.modules.webview.WebViewScreen
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHome
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHome
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHomeBroken
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirrorScreenRegular
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirrorScreenRegular
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirroringScreen
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicDashboard
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicDashboard
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibraryOutline
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSettingTwotone
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSound
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSoundTwotone
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSoundTwotone
|
||||||
import dev.krtirtho.spotube.resources.iconsax.User
|
import dev.krtirtho.spotube.resources.iconsax.User
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.navigation3.koinEntryProvider
|
import org.koin.compose.navigation3.koinEntryProvider
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.annotation.KoinExperimentalAPI
|
import org.koin.core.annotation.KoinExperimentalAPI
|
||||||
|
|
||||||
interface NavigationItem {
|
interface NavigationItem {
|
||||||
@ -138,6 +143,8 @@ fun App(
|
|||||||
) {
|
) {
|
||||||
val settingsRepository: SettingsRepository = koinInject<SettingsRepository>()
|
val settingsRepository: SettingsRepository = koinInject<SettingsRepository>()
|
||||||
val userSettings by settingsRepository.userSettings.collectAsStateWithLifecycle(initialValue = UserSettings())
|
val userSettings by settingsRepository.userSettings.collectAsStateWithLifecycle(initialValue = UserSettings())
|
||||||
|
val updateCheckerViewModel: UpdateCheckerViewModel = koinViewModel()
|
||||||
|
val availableUpdate by updateCheckerViewModel.availableUpdate.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val navigationState = rememberNavigationState(
|
val navigationState = rememberNavigationState(
|
||||||
startRoute = Routes.Home, topLevelRoutes = TOP_LEVEL_ROUTES
|
startRoute = Routes.Home, topLevelRoutes = TOP_LEVEL_ROUTES
|
||||||
@ -175,6 +182,43 @@ fun App(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
content()
|
content()
|
||||||
|
if (availableUpdate != null) {
|
||||||
|
val update = availableUpdate!!
|
||||||
|
ThemedDialog(
|
||||||
|
onDismissRequest = updateCheckerViewModel::dismissUpdate,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Spotube ${update.tag} is available",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
OutlineButton(onClick = updateCheckerViewModel::ignoreUpdate) {
|
||||||
|
Text("Ignore")
|
||||||
|
}
|
||||||
|
PrimaryButton(
|
||||||
|
onClick = {
|
||||||
|
runCatching { openUrlInBrowser("https://spotube.cc/downloads/") }
|
||||||
|
updateCheckerViewModel.dismissUpdate()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("Update")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (update.releaseNotesMarkdown.isNotBlank()) {
|
||||||
|
SelectionContainer {
|
||||||
|
Markdown(
|
||||||
|
content = update.releaseNotesMarkdown,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("See what's new in this release on the Spotube downloads page.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
|||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||||
import dev.krtirtho.spotube.modules.search.SearchRepository
|
import dev.krtirtho.spotube.modules.search.SearchRepository
|
||||||
import dev.krtirtho.spotube.modules.search.SearchScreenViewModel
|
import dev.krtirtho.spotube.modules.search.SearchScreenViewModel
|
||||||
|
import dev.krtirtho.spotube.modules.update.UpdateCheckerViewModel
|
||||||
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||||
import dev.krtirtho.spotube.modules.settings.UserSettingsSource
|
import dev.krtirtho.spotube.modules.settings.UserSettingsSource
|
||||||
@ -135,6 +136,7 @@ val sharedModules = module {
|
|||||||
singleOf(::SettingsRepository) { bind<UserSettingsSource>() }
|
singleOf(::SettingsRepository) { bind<UserSettingsSource>() }
|
||||||
viewModelOf(::SettingsViewModel) { bind<SettingsProvider>() }
|
viewModelOf(::SettingsViewModel) { bind<SettingsProvider>() }
|
||||||
viewModelOf(::JamSettingsViewModel)
|
viewModelOf(::JamSettingsViewModel)
|
||||||
|
viewModelOf(::UpdateCheckerViewModel)
|
||||||
|
|
||||||
// Downloads
|
// Downloads
|
||||||
singleOf(::DownloadManager)
|
singleOf(::DownloadManager)
|
||||||
|
|||||||
@ -87,6 +87,7 @@ data class UserSettings(
|
|||||||
|
|
||||||
// Updates
|
// Updates
|
||||||
val autoCheckForUpdates: Boolean = true,
|
val autoCheckForUpdates: Boolean = true,
|
||||||
|
val ignoredUpdateVersion: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -59,4 +59,10 @@ class SettingsRepository(private val database: Database) : UserSettingsSource {
|
|||||||
prefs[SETTINGS_KEY] = Json.encodeToString(newSettings)
|
prefs[SETTINGS_KEY] = Json.encodeToString(newSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentSettings(): UserSettings {
|
||||||
|
val preferences = database.settingsDataStore.data.first()
|
||||||
|
val settingsJson = preferences[SETTINGS_KEY] ?: return UserSettings()
|
||||||
|
return Json.decodeFromString<UserSettings>(settingsJson)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.krtirtho.spotube.modules.update
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import dev.krtirtho.spotube.AppBuildInfo
|
||||||
|
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import net.swiftzer.semver.SemVer
|
||||||
|
|
||||||
|
data class AvailableUpdate(
|
||||||
|
val tag: String,
|
||||||
|
val releaseNotesMarkdown: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
class UpdateCheckerViewModel(
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val logger = Logger.withTag("UpdateChecker")
|
||||||
|
private val httpClient = HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json { ignoreUnknownKeys = true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _availableUpdate = MutableStateFlow<AvailableUpdate?>(null)
|
||||||
|
val availableUpdate: StateFlow<AvailableUpdate?> = _availableUpdate.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
checkForUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkForUpdate() {
|
||||||
|
try {
|
||||||
|
// Read directly from DataStore so the check honors persisted preferences,
|
||||||
|
// rather than the repository StateFlow's default value during startup.
|
||||||
|
val settings = settingsRepository.getCurrentSettings()
|
||||||
|
if (!settings.autoCheckForUpdates) return
|
||||||
|
|
||||||
|
val release = httpClient.get(AppBuildInfo.LATEST_RELEASE_API_URL) {
|
||||||
|
header(HttpHeaders.Accept, "application/vnd.github+json")
|
||||||
|
header(HttpHeaders.UserAgent, "Spotube/${AppBuildInfo.VERSION}")
|
||||||
|
}.body<GitHubRelease>()
|
||||||
|
|
||||||
|
val latestVersion = parseVersion(release.tagName.removePrefix("v"))
|
||||||
|
?: return logger.w { "Ignoring release with invalid SemVer tag: ${release.tagName}" }
|
||||||
|
val currentVersion = parseVersion(AppBuildInfo.VERSION)
|
||||||
|
?: return logger.w { "Ignoring update check for invalid app version: ${AppBuildInfo.VERSION}" }
|
||||||
|
|
||||||
|
if (latestVersion > currentVersion && release.tagName != settings.ignoredUpdateVersion) {
|
||||||
|
_availableUpdate.value = AvailableUpdate(
|
||||||
|
tag = release.tagName,
|
||||||
|
releaseNotesMarkdown = release.body.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.w(e) { "Failed to check for app updates" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ignoreUpdate() {
|
||||||
|
val update = _availableUpdate.value ?: return
|
||||||
|
_availableUpdate.value = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching {
|
||||||
|
val settings = settingsRepository.getCurrentSettings()
|
||||||
|
settingsRepository.updateSettings(settings.copy(ignoredUpdateVersion = update.tag))
|
||||||
|
}.onFailure { e ->
|
||||||
|
logger.e(e) { "Failed to persist ignored app version" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissUpdate() {
|
||||||
|
_availableUpdate.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseVersion(version: String): SemVer? = runCatching {
|
||||||
|
SemVer.parse(version)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
httpClient.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class GitHubRelease(
|
||||||
|
@SerialName("tag_name") val tagName: String,
|
||||||
|
val body: String? = null,
|
||||||
|
)
|
||||||
25
composeApp/src/commonMain/templates/AppBuildInfo.kt.template
Normal file
25
composeApp/src/commonMain/templates/AppBuildInfo.kt.template
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.krtirtho.spotube
|
||||||
|
|
||||||
|
internal object AppBuildInfo {
|
||||||
|
const val VERSION = "${appVersion}"
|
||||||
|
const val BUILD_NUMBER = ${appBuildNumber}
|
||||||
|
const val REPOSITORY_URL = "${repositoryUrl}"
|
||||||
|
const val LATEST_RELEASE_API_URL = "${latestReleaseApiUrl}"
|
||||||
|
}
|
||||||
@ -46,6 +46,7 @@ kotlinx-serialization-json = "1.11.0"
|
|||||||
materialKolor = "4.1.1"
|
materialKolor = "4.1.1"
|
||||||
mqttClient = "2.1.1"
|
mqttClient = "2.1.1"
|
||||||
mqttBuffer = "6.30.8"
|
mqttBuffer = "6.30.8"
|
||||||
|
multiplatformMarkdownRenderer = "0.45.0"
|
||||||
murmurhash = "0.4.2"
|
murmurhash = "0.4.2"
|
||||||
newpipeextractor = "v0.26.5"
|
newpipeextractor = "v0.26.5"
|
||||||
newpipeExtractorKmp = "1.3.0"
|
newpipeExtractorKmp = "1.3.0"
|
||||||
@ -128,6 +129,8 @@ mqtt-x-models = { module = "com.ditchoom:mqtt-5-models", version.ref = "mqttClie
|
|||||||
mqtt-client = { module = "com.ditchoom:mqtt-client", version.ref = "mqttClient" }
|
mqtt-client = { module = "com.ditchoom:mqtt-client", version.ref = "mqttClient" }
|
||||||
mqtt-buffer = { module = "com.ditchoom:buffer", version.ref = "mqttBuffer" }
|
mqtt-buffer = { module = "com.ditchoom:buffer", version.ref = "mqttBuffer" }
|
||||||
mqtt-buffer-codec = { module = "com.ditchoom:buffer-codec", version.ref = "mqttBuffer" }
|
mqtt-buffer-codec = { module = "com.ditchoom:buffer-codec", version.ref = "mqttBuffer" }
|
||||||
|
multiplatform-markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "multiplatformMarkdownRenderer" }
|
||||||
|
multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRenderer" }
|
||||||
murmurhash = { module = "com.goncalossilva:murmurhash", version.ref = "murmurhash" }
|
murmurhash = { module = "com.goncalossilva:murmurhash", version.ref = "murmurhash" }
|
||||||
newpipe-extractor-kmp = { module = "io.github.yushosei:newpipe-extractor-kmp", version.ref = "newpipeExtractorKmp" }
|
newpipe-extractor-kmp = { module = "io.github.yushosei:newpipe-extractor-kmp", version.ref = "newpipeExtractorKmp" }
|
||||||
newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version.ref = "newpipeextractor" }
|
newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version.ref = "newpipeextractor" }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user