diff --git a/.github/workflows/Preview.yml b/.github/workflows/Preview.yml index 274efe3b..9dd6849d 100644 --- a/.github/workflows/Preview.yml +++ b/.github/workflows/Preview.yml @@ -86,6 +86,7 @@ jobs: -Pcompose.desktop.mac.notarization.appleID=${{ secrets.APPLE_ID }} -Pcompose.desktop.mac.notarization.password=${{ secrets.APPLE_PASSWORD }} -Pidentity="${{ secrets.APPLE_IDENTITY }}" + -Ppreview="${{ env.COMMIT_COUNT }}" --stacktrace # Upload runner package tar.gz/zip as artifact diff --git a/build.gradle.kts b/build.gradle.kts index 2b677f82..1c5a5376 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -230,9 +230,11 @@ buildConfig { buildConfigField("String", "NAME", project.name.wrap()) buildConfigField("String", "VERSION", project.version.toString().wrap()) buildConfigField("int", "MIGRATION_CODE", migrationCode.toString()) + buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString()) + buildConfigField("boolean", "IS_PREVIEW", project.hasProperty("preview").toString()) + buildConfigField("int", "PREVIEW_BUILD", project.properties["preview"]?.toString() ?: 0.toString()) // Tachidesk - buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString()) buildConfigField("String", "TACHIDESK_SP_VERSION", tachideskVersion.wrap()) buildConfigField("int", "SERVER_CODE", serverCode.toString()) } diff --git a/src/main/kotlin/ca/gosyer/data/DataModule.kt b/src/main/kotlin/ca/gosyer/data/DataModule.kt index 35298525..5564b877 100644 --- a/src/main/kotlin/ca/gosyer/data/DataModule.kt +++ b/src/main/kotlin/ca/gosyer/data/DataModule.kt @@ -32,6 +32,8 @@ import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.translation.ResourceProvider import ca.gosyer.data.translation.XmlResourceBundle import ca.gosyer.data.ui.UiPreferences +import ca.gosyer.data.update.UpdateChecker +import ca.gosyer.data.update.UpdatePreferences import io.kamel.core.config.KamelConfig import toothpick.ktp.binding.bind import toothpick.ktp.binding.module @@ -71,6 +73,10 @@ val DataModule = module { .toProviderInstance { MigrationPreferences(preferenceFactory.create("migration")) } .providesSingleton() + bind() + .toProviderInstance { UpdatePreferences(preferenceFactory.create("update")) } + .providesSingleton() + bind() .toProvider(HttpProvider::class) .providesSingleton() @@ -114,4 +120,7 @@ val DataModule = module { bind() .toClass() .singleton() + bind() + .toClass() + .singleton() } diff --git a/src/main/kotlin/ca/gosyer/data/update/UpdateChecker.kt b/src/main/kotlin/ca/gosyer/data/update/UpdateChecker.kt new file mode 100644 index 00000000..533a2530 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/update/UpdateChecker.kt @@ -0,0 +1,61 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.update + +import ca.gosyer.build.BuildConfig +import ca.gosyer.data.server.Http +import ca.gosyer.data.update.model.GithubRelease +import ca.gosyer.util.lang.launch +import ca.gosyer.util.lang.withIOContext +import io.ktor.client.request.get +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject + +class UpdateChecker @Inject constructor( + private val updatePreferences: UpdatePreferences, + private val client: Http +) { + val updateFound = MutableSharedFlow() + + @OptIn(DelicateCoroutinesApi::class) + fun checkForUpdates() { + if (!updatePreferences.enabled().get()) return + launch { + val latestRelease = withIOContext { + client.get("https://api.github.com/repos/$GITHUB_REPO/releases/latest") + } + if (isNewVersion(latestRelease.version)) { + updateFound.emit(latestRelease) + } + } + } + + // Thanks to Tachiyomi for inspiration + private fun isNewVersion(versionTag: String): Boolean { + // Removes prefixes like "r" or "v" + val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") + + return if (BuildConfig.IS_PREVIEW) { + // Preview builds: based on releases in "Suwayomi/Tachidesk-JUI-preview" repo + // tagged as something like "r123" + newVersion.toInt() > BuildConfig.PREVIEW_BUILD + } else { + // Release builds: based on releases in "Suwayomi/Tachidesk-JUI" repo + // tagged as something like "v1.1.2" + newVersion != BuildConfig.VERSION + } + } + + companion object { + private val GITHUB_REPO = if (BuildConfig.IS_PREVIEW) { + "Suwayomi/Tachidesk-JUI-preview" + } else { + "Suwayomi/Tachidesk-JUI" + } + } +} diff --git a/src/main/kotlin/ca/gosyer/data/update/UpdatePreferences.kt b/src/main/kotlin/ca/gosyer/data/update/UpdatePreferences.kt new file mode 100644 index 00000000..39af9d70 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/update/UpdatePreferences.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.update + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore + +class UpdatePreferences(private val preferenceStore: PreferenceStore) { + fun enabled(): Preference { + return preferenceStore.getBoolean("enabled", true) + } +} diff --git a/src/main/kotlin/ca/gosyer/data/update/model/GithubRelease.kt b/src/main/kotlin/ca/gosyer/data/update/model/GithubRelease.kt new file mode 100644 index 00000000..d37389dc --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/update/model/GithubRelease.kt @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.update.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GithubRelease( + @SerialName("tag_name") val version: String, + @SerialName("body") val info: String, + @SerialName("html_url") val releaseLink: String +) diff --git a/src/main/kotlin/ca/gosyer/ui/main/main.kt b/src/main/kotlin/ca/gosyer/ui/main/main.kt index 08f34aa8..c88e0538 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/main.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/main.kt @@ -12,6 +12,7 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -21,6 +22,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource +import androidx.compose.ui.window.Notification import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.awaitApplication @@ -35,6 +37,7 @@ import ca.gosyer.data.server.ServerService.ServerResult import ca.gosyer.data.translation.XmlResourceBundle import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.model.ThemeMode +import ca.gosyer.data.update.UpdateChecker import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.prefs.asStateIn @@ -55,7 +58,9 @@ import io.kamel.image.config.LocalKamelConfig import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch import org.jetbrains.skiko.SystemTheme import org.jetbrains.skiko.currentSystemTheme import toothpick.configuration.Configuration @@ -89,6 +94,7 @@ suspend fun main() { val serverService = scope.getInstance() val uiPreferences = scope.getInstance() + val updateChecker = scope.getInstance() // Call setDefault before getting a resource bundle val language = uiPreferences.language().get() @@ -165,6 +171,21 @@ suspend fun main() { } ) + LaunchedEffect(Unit) { + launch { + updateChecker.checkForUpdates() + updateChecker.updateFound.collect { + trayState.sendNotification( + Notification( + resources.getStringA("new_update_title"), + resources.getString("new_update_message", it.version), + Notification.Type.Info + ) + ) + } + } + } + Window( onCloseRequest = { if (confirmExit.value) { diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt index cfd826e4..bab508c2 100644 --- a/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt @@ -9,15 +9,30 @@ package ca.gosyer.ui.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import ca.gosyer.data.update.UpdatePreferences import ca.gosyer.ui.base.components.MenuController import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.resources.stringResource +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.base.vm.viewModel +import javax.inject.Inject + +class SettingsAdvancedViewModel @Inject constructor( + updatePreferences: UpdatePreferences, +) : ViewModel() { + val updatesEnabled = updatePreferences.enabled().asStateFlow() +} @Composable fun SettingsAdvancedScreen(menuController: MenuController) { + val vm = viewModel() Column { Toolbar(stringResource("settings_advanced_screen"), menuController, true) LazyColumn { + item { + SwitchPreference(preference = vm.updatesEnabled, title = stringResource("update_checker")) + } } } } diff --git a/src/main/resources/values/values/strings.xml b/src/main/resources/values/values/strings.xml index 729f4ce5..d6825862 100644 --- a/src/main/resources/values/values/strings.xml +++ b/src/main/resources/values/values/strings.xml @@ -12,6 +12,8 @@ Cannot start server Confirm exit Are you sure you want to exit? + Tachidesk-JUI update available + %1$s is now available! Yes @@ -219,4 +221,7 @@ Digest auth Auth username Auth password + + + Check for updates