mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Show toast & add option to fetch release notes when app updates (#1182)
## Description When Wholphin is updated, it will show a short toast message. This works for both app store and side loaded installs. You can now click on the "Installed version" in settings to fetch the release notes for the current version. ### Related issues Closes #1058 ### Testing Emulator ## Screenshots  ## AI or LLM usage None
This commit is contained in:
parent
46beee00bb
commit
feb8c31791
7 changed files with 207 additions and 85 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
|
|
@ -51,14 +52,15 @@ import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
|||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
||||
import com.github.damontecres.wholphin.util.DebugLogTree
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
|
@ -394,6 +396,7 @@ class MainActivity : AppCompatActivity() {
|
|||
class MainActivityViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val preferences: DataStore<AppPreferences>,
|
||||
val serverRepository: ServerRepository,
|
||||
private val navigationManager: SetupNavigationManager,
|
||||
|
|
@ -402,9 +405,19 @@ class MainActivityViewModel
|
|||
private val appUpgradeHandler: AppUpgradeHandler,
|
||||
) : ViewModel() {
|
||||
fun appStart() {
|
||||
viewModelScope.launchIO {
|
||||
viewModelScope.launchDefault {
|
||||
try {
|
||||
val needUpgrade = appUpgradeHandler.needUpgrade()
|
||||
if (needUpgrade) {
|
||||
showToast(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.updated_toast,
|
||||
appUpgradeHandler.currentVersion.toString(),
|
||||
),
|
||||
)
|
||||
appUpgradeHandler.run()
|
||||
}
|
||||
appUpgradeHandler.copySubfont(false)
|
||||
val prefs =
|
||||
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
|
|
@ -447,7 +460,7 @@ class MainActivityViewModel
|
|||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
viewModelScope.launchDefault {
|
||||
// Create the mediaCodecCapabilitiesTest if needed
|
||||
deviceProfileService.mediaCodecCapabilitiesTest.supportsAVC()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Build
|
||||
import androidx.core.content.edit
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.github.damontecres.wholphin.WholphinApplication
|
||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
|
|
@ -43,12 +43,10 @@ class AppUpgradeHandler
|
|||
private val appPreferences: DataStore<AppPreferences>,
|
||||
private val seerrServerDao: SeerrServerDao,
|
||||
) {
|
||||
suspend fun run() {
|
||||
val pkgInfo =
|
||||
WholphinApplication.instance.packageManager.getPackageInfo(
|
||||
WholphinApplication.instance.packageName,
|
||||
0,
|
||||
)
|
||||
val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!)
|
||||
|
||||
fun needUpgrade(): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
|
||||
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
|
||||
|
|
@ -60,10 +58,25 @@ class AppUpgradeHandler
|
|||
} else {
|
||||
pkgInfo.versionCode.toLong()
|
||||
}
|
||||
if (newVersion != previousVersion || newVersionCode != previousVersionCode) {
|
||||
Timber.i(
|
||||
"App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
|
||||
"App versions: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
|
||||
)
|
||||
return newVersion != previousVersion || newVersionCode != previousVersionCode
|
||||
}
|
||||
|
||||
suspend fun run() {
|
||||
Timber.i("App upgrade started")
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
|
||||
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
|
||||
|
||||
val newVersion = pkgInfo.versionName!!
|
||||
val newVersionCode =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
pkgInfo.longVersionCode
|
||||
} else {
|
||||
pkgInfo.versionCode.toLong()
|
||||
}
|
||||
// Store the previous and new version info
|
||||
prefs.edit(true) {
|
||||
putString(VERSION_NAME_PREVIOUS_KEY, previousVersion)
|
||||
|
|
@ -82,9 +95,6 @@ class AppUpgradeHandler
|
|||
Timber.e(ex, "Exception during app upgrade")
|
||||
}
|
||||
Timber.i("App upgrade complete")
|
||||
} else {
|
||||
Timber.d("No app update needed")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -128,18 +128,36 @@ class UpdateChecker
|
|||
return Version.fromString(pkgInfo.versionName!!)
|
||||
}
|
||||
|
||||
suspend fun getRelease(version: Version): Release? {
|
||||
val url =
|
||||
"https://api.github.com/repos/damontecres/Wholphin/releases/tags/v${version.major}.${version.minor}.${version.patch}"
|
||||
return withContext(Dispatchers.IO) {
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
getRelease(request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest released version
|
||||
*/
|
||||
suspend fun getLatestRelease(updateUrl: String): Release? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
suspend fun getLatestRelease(updateUrl: String): Release? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(updateUrl)
|
||||
.get()
|
||||
.build()
|
||||
okHttpClient.newCall(request).execute().use {
|
||||
getRelease(request)
|
||||
}
|
||||
|
||||
private fun getRelease(request: Request): Release? {
|
||||
return okHttpClient.newCall(request).execute().use {
|
||||
if (it.isSuccessful && it.body != null) {
|
||||
val result = Json.parseToJsonElement(it.body!!.string())
|
||||
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
|
||||
|
|
@ -161,19 +179,18 @@ class UpdateChecker
|
|||
m.groupValues[1]
|
||||
}.toList()
|
||||
} else {
|
||||
listOf()
|
||||
emptyList()
|
||||
}
|
||||
return@use Release(version, downloadUrl, publishedAt, body, notes)
|
||||
} else {
|
||||
Timber.w("Update version parsing failed. name=$name")
|
||||
}
|
||||
} else {
|
||||
Timber.w("Update check failed: ${it.message}")
|
||||
Timber.w("Update check failed ${it.code}: ${it.message}")
|
||||
}
|
||||
return@use null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and install an update
|
||||
|
|
@ -347,7 +364,19 @@ data class Release(
|
|||
val publishedAt: String?,
|
||||
val body: String?,
|
||||
val notes: List<String>,
|
||||
) {
|
||||
val content =
|
||||
"# ${version}\n" +
|
||||
(
|
||||
(notes.joinToString("\n").takeIf { it.isNotNullOrBlank() } ?: "") +
|
||||
(body ?: "")
|
||||
).replace(
|
||||
Regex("https://github.com/\\w*/\\w+/pull/(\\d+)"),
|
||||
"#$1",
|
||||
)
|
||||
// Remove the last line for full changelog since its just a link
|
||||
.replace(Regex("\\*\\*Full Changelog\\*\\*.*"), "")
|
||||
}
|
||||
|
||||
interface DownloadCallback {
|
||||
fun contentLength(contentLength: Long)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -55,20 +54,26 @@ import com.github.damontecres.wholphin.preferences.advancedPreferences
|
|||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
||||
import com.github.damontecres.wholphin.preferences.screensaverPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||
import com.github.damontecres.wholphin.services.Release
|
||||
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.ScrollableDialog
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playOnClickSound
|
||||
import com.github.damontecres.wholphin.ui.playSoundOnFocus
|
||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||
import com.github.damontecres.wholphin.ui.setup.ReleaseNotes
|
||||
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
|
||||
import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog
|
||||
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -93,6 +98,7 @@ fun PreferencesContent(
|
|||
val currentUser by viewModel.currentUser.observeAsState()
|
||||
val currentServer by seerrVm.currentSeerrServer.collectAsState(null)
|
||||
var showPinFlow by remember { mutableStateOf(false) }
|
||||
var showVersionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
|
||||
val seerrConnection by viewModel.seerrConnection.collectAsState()
|
||||
|
|
@ -252,15 +258,13 @@ fun PreferencesContent(
|
|||
}
|
||||
when (pref) {
|
||||
AppPreference.InstalledVersion -> {
|
||||
var clickCount by remember { mutableIntStateOf(0) }
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.installed_version),
|
||||
onClick = {
|
||||
if (movementSounds) playOnClickSound(context)
|
||||
if (clickCount++ >= 2) {
|
||||
clickCount = 0
|
||||
showVersionDialog = true
|
||||
},
|
||||
onLongClick = {
|
||||
viewModel.navigationManager.navigateTo(Destination.Debug)
|
||||
}
|
||||
},
|
||||
summary = installedVersion.toString(),
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -615,6 +619,33 @@ fun PreferencesContent(
|
|||
},
|
||||
)
|
||||
}
|
||||
if (showVersionDialog) {
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.fetchReleaseNotes()
|
||||
}
|
||||
val release by viewModel.releaseNotes.collectAsState()
|
||||
ScrollableDialog(
|
||||
onDismissRequest = { showVersionDialog = false },
|
||||
) {
|
||||
item {
|
||||
when (val r = release) {
|
||||
is DataLoadingState.Error -> {
|
||||
ErrorMessage(message = "Error", exception = r.exception)
|
||||
}
|
||||
|
||||
DataLoadingState.Pending,
|
||||
DataLoadingState.Loading,
|
||||
-> {
|
||||
LoadingPage()
|
||||
}
|
||||
|
||||
is DataLoadingState.Success<Release> -> {
|
||||
ReleaseNotes(r.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ import com.github.damontecres.wholphin.preferences.resetSubtitles
|
|||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.Release
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.RememberTabManager
|
||||
|
|
@ -22,9 +25,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -42,6 +47,7 @@ class PreferencesViewModel
|
|||
private val seerrServerRepository: SeerrServerRepository,
|
||||
private val deviceInfo: DeviceInfo,
|
||||
private val clientInfo: ClientInfo,
|
||||
private val updateChecker: UpdateChecker,
|
||||
) : ViewModel(),
|
||||
RememberTabManager by rememberTabManager {
|
||||
val currentUser get() = serverRepository.currentUser
|
||||
|
|
@ -51,6 +57,8 @@ class PreferencesViewModel
|
|||
private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
||||
val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus
|
||||
|
||||
val releaseNotes = MutableStateFlow<DataLoadingState<Release>>(DataLoadingState.Pending)
|
||||
|
||||
init {
|
||||
viewModelScope.launchIO {
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
|
|
@ -99,6 +107,23 @@ class PreferencesViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun fetchReleaseNotes() {
|
||||
viewModelScope.launchIO {
|
||||
releaseNotes.update { DataLoadingState.Loading }
|
||||
try {
|
||||
val release = updateChecker.getRelease(updateChecker.getInstalledVersion())
|
||||
if (release != null) {
|
||||
releaseNotes.update { DataLoadingState.Success(release) }
|
||||
} else {
|
||||
releaseNotes.update { DataLoadingState.Error("Release not found") }
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error fetching release")
|
||||
releaseNotes.update { DataLoadingState.Error(ex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
|
||||
appPreferences.updateData {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ import com.github.damontecres.wholphin.services.Release
|
|||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
import com.github.damontecres.wholphin.ui.components.Button
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.TextButton
|
||||
|
|
@ -66,6 +65,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
|||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.Version
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -272,15 +272,7 @@ fun InstallUpdatePageContent(
|
|||
},
|
||||
) {
|
||||
item {
|
||||
Markdown(
|
||||
(release.notes.joinToString("\n\n") + (release.body ?: ""))
|
||||
.replace(
|
||||
Regex("https://github.com/damontecres/\\w+/pull/(\\d+)"),
|
||||
"#$1",
|
||||
)
|
||||
// Remove the last line for full changelog since its just a link
|
||||
.replace(Regex("\\*\\*Full Changelog\\*\\*.*"), ""),
|
||||
)
|
||||
ReleaseNotes(release)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
|
|
@ -317,6 +309,25 @@ fun InstallUpdatePageContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReleaseNotes(
|
||||
release: Release,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Markdown(
|
||||
content = release.content,
|
||||
typography =
|
||||
markdownTypography(
|
||||
h1 = MaterialTheme.typography.headlineLarge,
|
||||
h2 = MaterialTheme.typography.headlineMedium,
|
||||
h3 = MaterialTheme.typography.headlineSmall,
|
||||
text = MaterialTheme.typography.bodySmall,
|
||||
code = MaterialTheme.typography.bodySmall,
|
||||
),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadDialog(
|
||||
contentLength: Long,
|
||||
|
|
@ -392,6 +403,7 @@ private fun InstallUpdatePageContentPreview() {
|
|||
downloadUrl = "https://url",
|
||||
publishedAt = null,
|
||||
body =
|
||||
"## Header 2\n" +
|
||||
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " +
|
||||
"ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " +
|
||||
"tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " +
|
||||
|
|
@ -399,6 +411,7 @@ private fun InstallUpdatePageContentPreview() {
|
|||
"Iaculis massa nisl malesuada lacinia integer nunc posuere. " +
|
||||
"Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora " +
|
||||
"torquent per conubia nostra inceptos himenaeos.\n\n" +
|
||||
"### Header 3\n" +
|
||||
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " +
|
||||
"ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " +
|
||||
"tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " +
|
||||
|
|
|
|||
|
|
@ -748,5 +748,6 @@
|
|||
<string name="select_all">Select all</string>
|
||||
<string name="separate_types">Separate types</string>
|
||||
<string name="prefer_logos">Prefer showing logos for titles</string>
|
||||
<string name="updated_toast">Wholphin updated to %s</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue