diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index e8791bde..44b0bcb9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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, 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 { - appUpgradeHandler.run() + 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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index c1507d9f..23093cfd 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -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, 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,31 +58,43 @@ class AppUpgradeHandler } else { pkgInfo.versionCode.toLong() } - if (newVersion != previousVersion || newVersionCode != previousVersionCode) { - Timber.i( - "App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode", - ) - // Store the previous and new version info - prefs.edit(true) { - putString(VERSION_NAME_PREVIOUS_KEY, previousVersion) - putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode) - putString(VERSION_NAME_CURRENT_KEY, newVersion) - putLong(VERSION_CODE_CURRENT_KEY, newVersionCode) + Timber.i( + "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() } - 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") - } else { - Timber.d("No app update needed") + // Store the previous and new version info + prefs.edit(true) { + putString(VERSION_NAME_PREVIOUS_KEY, previousVersion) + putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode) + putString(VERSION_NAME_CURRENT_KEY, newVersion) + putLong(VERSION_CODE_CURRENT_KEY, newVersionCode) } + 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") } /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 9e0665dd..f1701416 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -128,50 +128,67 @@ 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 { - if (it.isSuccessful && it.body != null) { - val result = Json.parseToJsonElement(it.body!!.string()) - val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull - val version = Version.tryFromString(name) - val publishedAt = - result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull - val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull - val downloadUrl = - result.jsonObject["assets"] - ?.jsonArray - ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } - Timber.v("version=$version, downloadUrl=$downloadUrl") - if (version != null) { - val notes = - if (body.isNotNullOrBlank()) { - NOTE_REGEX - .findAll(body) - .map { m -> - m.groupValues[1] - }.toList() - } else { - listOf() - } - return@use Release(version, downloadUrl, publishedAt, body, notes) - } else { - Timber.w("Update version parsing failed. name=$name") - } + 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 + val version = Version.tryFromString(name) + val publishedAt = + result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull + val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull + val downloadUrl = + result.jsonObject["assets"] + ?.jsonArray + ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } + Timber.v("version=$version, downloadUrl=$downloadUrl") + if (version != null) { + val notes = + if (body.isNotNullOrBlank()) { + NOTE_REGEX + .findAll(body) + .map { m -> + m.groupValues[1] + }.toList() + } else { + emptyList() + } + return@use Release(version, downloadUrl, publishedAt, body, notes) } 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 body: String?, val notes: List, -) +) { + 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) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index c214d3cb..18b630f6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -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 - viewModel.navigationManager.navigateTo(Destination.Debug) - } + 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 -> { + ReleaseNotes(r.data) + } + } + } + } + } } @Composable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt index 58f9a5e3..80dd7934 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt @@ -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.Pending) val quickConnectStatus: StateFlow = _quickConnectStatus + val releaseNotes = MutableStateFlow>(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.updateData { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt index cc6f2d3d..7e5a6ec5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/InstallUpdatePage.kt @@ -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,13 +403,15 @@ private fun InstallUpdatePageContentPreview() { downloadUrl = "https://url", publishedAt = null, 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 " + "tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. " + "Pulvinar vivamus fringilla lacus nec metus bibendum egestas. " + "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. " + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7dbbf3b..ce4bf2b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -748,5 +748,6 @@ Select all Separate types Prefer showing logos for titles + Wholphin updated to %s