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
![release_notes
Large](https://github.com/user-attachments/assets/2c03eb74-abb2-4b45-8d6f-01e8ef72116c)

## AI or LLM usage
None
This commit is contained in:
Ray 2026-04-03 12:44:39 -04:00 committed by GitHub
parent 46beee00bb
commit feb8c31791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 207 additions and 85 deletions

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin package com.github.damontecres.wholphin
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle 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.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchDefault 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.nav.Destination
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
import com.github.damontecres.wholphin.util.DebugLogTree import com.github.damontecres.wholphin.util.DebugLogTree
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
@ -394,6 +396,7 @@ class MainActivity : AppCompatActivity() {
class MainActivityViewModel class MainActivityViewModel
@Inject @Inject
constructor( constructor(
@param:ApplicationContext private val context: Context,
private val preferences: DataStore<AppPreferences>, private val preferences: DataStore<AppPreferences>,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
private val navigationManager: SetupNavigationManager, private val navigationManager: SetupNavigationManager,
@ -402,9 +405,19 @@ class MainActivityViewModel
private val appUpgradeHandler: AppUpgradeHandler, private val appUpgradeHandler: AppUpgradeHandler,
) : ViewModel() { ) : ViewModel() {
fun appStart() { fun appStart() {
viewModelScope.launchIO { viewModelScope.launchDefault {
try { try {
appUpgradeHandler.run() val needUpgrade = appUpgradeHandler.needUpgrade()
if (needUpgrade) {
showToast(
context,
context.getString(
R.string.updated_toast,
appUpgradeHandler.currentVersion.toString(),
),
)
appUpgradeHandler.run()
}
appUpgradeHandler.copySubfont(false) appUpgradeHandler.copySubfont(false)
val prefs = val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
@ -447,7 +460,7 @@ class MainActivityViewModel
navigationManager.navigateTo(SetupDestination.ServerList) navigationManager.navigateTo(SetupDestination.ServerList)
} }
} }
viewModelScope.launchIO { viewModelScope.launchDefault {
// Create the mediaCodecCapabilitiesTest if needed // Create the mediaCodecCapabilitiesTest if needed
deviceProfileService.mediaCodecCapabilitiesTest.supportsAVC() deviceProfileService.mediaCodecCapabilitiesTest.supportsAVC()
} }

View file

@ -1,11 +1,11 @@
package com.github.damontecres.wholphin.services package com.github.damontecres.wholphin.services
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo
import android.os.Build import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.WholphinApplication
import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.data.SeerrServerDao
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
@ -43,12 +43,10 @@ class AppUpgradeHandler
private val appPreferences: DataStore<AppPreferences>, private val appPreferences: DataStore<AppPreferences>,
private val seerrServerDao: SeerrServerDao, private val seerrServerDao: SeerrServerDao,
) { ) {
suspend fun run() { val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
val pkgInfo = val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!)
WholphinApplication.instance.packageManager.getPackageInfo(
WholphinApplication.instance.packageName, fun needUpgrade(): Boolean {
0,
)
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null) val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1) val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
@ -60,31 +58,43 @@ class AppUpgradeHandler
} else { } else {
pkgInfo.versionCode.toLong() pkgInfo.versionCode.toLong()
} }
if (newVersion != previousVersion || newVersionCode != previousVersionCode) { Timber.i(
Timber.i( "App versions: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
"App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode", )
) return newVersion != previousVersion || newVersionCode != previousVersionCode
// Store the previous and new version info }
prefs.edit(true) {
putString(VERSION_NAME_PREVIOUS_KEY, previousVersion) suspend fun run() {
putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode) Timber.i("App upgrade started")
putString(VERSION_NAME_CURRENT_KEY, newVersion) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode) 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()
} }
try { // Store the previous and new version info
copySubfont(true) prefs.edit(true) {
upgradeApp( putString(VERSION_NAME_PREVIOUS_KEY, previousVersion)
Version.fromString(previousVersion ?: "0.0.0"), putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode)
Version.fromString(newVersion), putString(VERSION_NAME_CURRENT_KEY, newVersion)
appPreferences, putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
)
} catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade")
}
Timber.i("App upgrade complete")
} else {
Timber.d("No app update needed")
} }
try {
copySubfont(true)
upgradeApp(
Version.fromString(previousVersion ?: "0.0.0"),
Version.fromString(newVersion),
appPreferences,
)
} catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade")
}
Timber.i("App upgrade complete")
} }
/** /**

View file

@ -128,50 +128,67 @@ class UpdateChecker
return Version.fromString(pkgInfo.versionName!!) 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 * Get the latest released version
*/ */
suspend fun getLatestRelease(updateUrl: String): Release? { suspend fun getLatestRelease(updateUrl: String): Release? =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val request = val request =
Request Request
.Builder() .Builder()
.url(updateUrl) .url(updateUrl)
.get() .get()
.build() .build()
okHttpClient.newCall(request).execute().use { getRelease(request)
if (it.isSuccessful && it.body != null) { }
val result = Json.parseToJsonElement(it.body!!.string())
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull private fun getRelease(request: Request): Release? {
val version = Version.tryFromString(name) return okHttpClient.newCall(request).execute().use {
val publishedAt = if (it.isSuccessful && it.body != null) {
result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull val result = Json.parseToJsonElement(it.body!!.string())
val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
val downloadUrl = val version = Version.tryFromString(name)
result.jsonObject["assets"] val publishedAt =
?.jsonArray result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull
?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull
Timber.v("version=$version, downloadUrl=$downloadUrl") val downloadUrl =
if (version != null) { result.jsonObject["assets"]
val notes = ?.jsonArray
if (body.isNotNullOrBlank()) { ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) }
NOTE_REGEX Timber.v("version=$version, downloadUrl=$downloadUrl")
.findAll(body) if (version != null) {
.map { m -> val notes =
m.groupValues[1] if (body.isNotNullOrBlank()) {
}.toList() NOTE_REGEX
} else { .findAll(body)
listOf() .map { m ->
} m.groupValues[1]
return@use Release(version, downloadUrl, publishedAt, body, notes) }.toList()
} else { } else {
Timber.w("Update version parsing failed. name=$name") emptyList()
} }
return@use Release(version, downloadUrl, publishedAt, body, notes)
} else { } else {
Timber.w("Update check failed: ${it.message}") Timber.w("Update version parsing failed. name=$name")
} }
return@use null } else {
Timber.w("Update check failed ${it.code}: ${it.message}")
} }
return@use null
} }
} }
@ -347,7 +364,19 @@ data class Release(
val publishedAt: String?, val publishedAt: String?,
val body: String?, val body: String?,
val notes: List<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 { interface DownloadCallback {
fun contentLength(contentLength: Long) fun contentLength(contentLength: Long)

View file

@ -24,7 +24,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.basicPreferences
import com.github.damontecres.wholphin.preferences.screensaverPreferences import com.github.damontecres.wholphin.preferences.screensaverPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences 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.SeerrConnectionStatus
import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.components.ConfirmDialog 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.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playOnClickSound import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings 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.UpdateViewModel
import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.ui.tryRequestFocus 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.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -93,6 +98,7 @@ fun PreferencesContent(
val currentUser by viewModel.currentUser.observeAsState() val currentUser by viewModel.currentUser.observeAsState()
val currentServer by seerrVm.currentSeerrServer.collectAsState(null) val currentServer by seerrVm.currentSeerrServer.collectAsState(null)
var showPinFlow by remember { mutableStateOf(false) } var showPinFlow by remember { mutableStateOf(false) }
var showVersionDialog by remember { mutableStateOf(false) }
var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) } var cacheUsage by remember { mutableStateOf(CacheUsage(0, 0, 0)) }
val seerrConnection by viewModel.seerrConnection.collectAsState() val seerrConnection by viewModel.seerrConnection.collectAsState()
@ -252,15 +258,13 @@ fun PreferencesContent(
} }
when (pref) { when (pref) {
AppPreference.InstalledVersion -> { AppPreference.InstalledVersion -> {
var clickCount by remember { mutableIntStateOf(0) }
ClickPreference( ClickPreference(
title = stringResource(R.string.installed_version), title = stringResource(R.string.installed_version),
onClick = { onClick = {
if (movementSounds) playOnClickSound(context) showVersionDialog = true
if (clickCount++ >= 2) { },
clickCount = 0 onLongClick = {
viewModel.navigationManager.navigateTo(Destination.Debug) viewModel.navigationManager.navigateTo(Destination.Debug)
}
}, },
summary = installedVersion.toString(), summary = installedVersion.toString(),
interactionSource = interactionSource, 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 @Composable

View file

@ -11,10 +11,13 @@ import com.github.damontecres.wholphin.preferences.resetSubtitles
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager 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.ScreensaverService
import com.github.damontecres.wholphin.services.SeerrServerRepository 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.detail.DebugViewModel.Companion.sendAppLogs
import com.github.damontecres.wholphin.ui.launchIO 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.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.RememberTabManager import com.github.damontecres.wholphin.util.RememberTabManager
@ -22,9 +25,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.DeviceInfo
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -42,6 +47,7 @@ class PreferencesViewModel
private val seerrServerRepository: SeerrServerRepository, private val seerrServerRepository: SeerrServerRepository,
private val deviceInfo: DeviceInfo, private val deviceInfo: DeviceInfo,
private val clientInfo: ClientInfo, private val clientInfo: ClientInfo,
private val updateChecker: UpdateChecker,
) : ViewModel(), ) : ViewModel(),
RememberTabManager by rememberTabManager { RememberTabManager by rememberTabManager {
val currentUser get() = serverRepository.currentUser val currentUser get() = serverRepository.currentUser
@ -51,6 +57,8 @@ class PreferencesViewModel
private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending) private val _quickConnectStatus = MutableStateFlow<LoadingState>(LoadingState.Pending)
val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus val quickConnectStatus: StateFlow<LoadingState> = _quickConnectStatus
val releaseNotes = MutableStateFlow<DataLoadingState<Release>>(DataLoadingState.Pending)
init { init {
viewModelScope.launchIO { viewModelScope.launchIO {
serverRepository.currentUser.value?.let { user -> 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 { companion object {
suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) { suspend fun resetSubtitleSettings(appPreferences: DataStore<AppPreferences>) {
appPreferences.updateData { appPreferences.updateData {

View file

@ -53,7 +53,6 @@ import com.github.damontecres.wholphin.services.Release
import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.BasicDialog 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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TextButton 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.LoadingState
import com.github.damontecres.wholphin.util.Version import com.github.damontecres.wholphin.util.Version
import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -272,15 +272,7 @@ fun InstallUpdatePageContent(
}, },
) { ) {
item { item {
Markdown( ReleaseNotes(release)
(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\\*\\*.*"), ""),
)
} }
} }
Column( 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 @Composable
fun DownloadDialog( fun DownloadDialog(
contentLength: Long, contentLength: Long,
@ -392,13 +403,15 @@ private fun InstallUpdatePageContentPreview() {
downloadUrl = "https://url", downloadUrl = "https://url",
publishedAt = null, publishedAt = null,
body = body =
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " + "## Header 2\n" +
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " +
"ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " + "ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " +
"tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " + "tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " +
"Pulvinar vivamus fringilla lacus nec metus bibendum egestas. " + "Pulvinar vivamus fringilla lacus nec metus bibendum egestas. " +
"Iaculis massa nisl malesuada lacinia integer nunc posuere. " + "Iaculis massa nisl malesuada lacinia integer nunc posuere. " +
"Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora " + "Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora " +
"torquent per conubia nostra inceptos himenaeos.\n\n" + "torquent per conubia nostra inceptos himenaeos.\n\n" +
"### Header 3\n" +
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " + "Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus " +
"ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " + "ex sapien vitae pellentesque sem placerat. In id cursus mi pretium " +
"tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " + "tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " +

View file

@ -748,5 +748,6 @@
<string name="select_all">Select all</string> <string name="select_all">Select all</string>
<string name="separate_types">Separate types</string> <string name="separate_types">Separate types</string>
<string name="prefer_logos">Prefer showing logos for titles</string> <string name="prefer_logos">Prefer showing logos for titles</string>
<string name="updated_toast">Wholphin updated to %s</string>
</resources> </resources>