mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
added in app update (#319)
This commit is contained in:
parent
8a24d5e06d
commit
4d5a38938c
@ -45,7 +45,7 @@ dependencies {
|
||||
implementation(libs.androidx.datastore)
|
||||
|
||||
implementation(libs.aboutLibraries.core)
|
||||
|
||||
implementation(libs.markdownRenderer.core)
|
||||
implementation(libs.composeFileKit) {
|
||||
exclude(group = "net.java.dev.jna")
|
||||
}
|
||||
@ -180,13 +180,17 @@ buildConfig {
|
||||
getApplicationPackageName()
|
||||
}
|
||||
)
|
||||
buildConfigField(
|
||||
"APP_DISPLAY_NAME",
|
||||
provider { getPrettifiedAppName() }
|
||||
)
|
||||
buildConfigField(
|
||||
"APP_VERSION",
|
||||
provider { getAppVersionString() }
|
||||
)
|
||||
buildConfigField(
|
||||
"APP_NAME",
|
||||
provider { getPrettifiedAppName() }
|
||||
provider { getAppName() }
|
||||
)
|
||||
buildConfigField(
|
||||
"PROJECT_WEBSITE",
|
||||
@ -200,6 +204,18 @@ buildConfig {
|
||||
"https://github.com/amir1376/ab-download-manager"
|
||||
}
|
||||
)
|
||||
buildConfigField(
|
||||
"PROJECT_GITHUB_OWNER",
|
||||
provider {
|
||||
"amir1376"
|
||||
}
|
||||
)
|
||||
buildConfigField(
|
||||
"PROJECT_GITHUB_REPO",
|
||||
provider {
|
||||
"ab-download-manager"
|
||||
}
|
||||
)
|
||||
buildConfigField(
|
||||
"PROJECT_TRANSLATIONS",
|
||||
provider {
|
||||
|
@ -1,2 +1,3 @@
|
||||
app.config.path="${user.home}/.abdm/config"
|
||||
app.system.path="${user.home}/.abdm/system"
|
||||
app.debug="false"
|
||||
|
@ -156,7 +156,13 @@ FunctionEnd
|
||||
Delete "$DESKTOP\${APP_DISPLAY_NAME}.lnk"
|
||||
!macroend
|
||||
|
||||
|
||||
Function .onInstSuccess
|
||||
; Check if the installer is running in silent mode
|
||||
${If} ${Silent}
|
||||
; In silent mode, always run the app
|
||||
Call RunMainBinary
|
||||
${Endif}
|
||||
FunctionEnd
|
||||
|
||||
Section "${APP_DISPLAY_NAME}"
|
||||
SectionInstType RO
|
||||
|
@ -3,13 +3,14 @@
|
||||
*/
|
||||
package com.abdownloadmanager.desktop
|
||||
|
||||
import com.abdownloadmanager.UpdateManager
|
||||
import com.abdownloadmanager.desktop.di.Di
|
||||
import com.abdownloadmanager.desktop.ui.Ui
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.singleInstance.*
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.sun.jna.platform.win32.Advapi32Util
|
||||
import com.abdownloadmanager.utils.appinfo.PreviousVersion
|
||||
import ir.amirab.util.platform.Platform
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.Path.Companion.toOkioPath
|
||||
@ -22,6 +23,8 @@ class App : AutoCloseable,
|
||||
KoinComponent {
|
||||
private val downloadSystem: DownloadSystem by inject()
|
||||
private val integration: Integration by inject()
|
||||
private val previousVersion: PreviousVersion by inject()
|
||||
private val updateManager: UpdateManager by inject()
|
||||
|
||||
//TODO Setup Native Messaging Feature
|
||||
//private val browserNativeMessaging: NativeMessaging by inject()
|
||||
@ -34,8 +37,10 @@ class App : AutoCloseable,
|
||||
runBlocking {
|
||||
//make sure to not get any dependency until boot the DI Container
|
||||
Di.boot()
|
||||
// it's better to organize these list of boot functions in a separate class
|
||||
integration.boot()
|
||||
downloadSystem.boot()
|
||||
previousVersion.boot()
|
||||
//TODO Setup Native Messaging Feature
|
||||
//waiting for compose kmp to add multi launcher to nativeDistributions,the PR is already exists but not merger
|
||||
//or maybe I should use a custom solution
|
||||
@ -79,7 +84,7 @@ fun main(args: Array<String>) {
|
||||
appArguments = appArguments,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
System.err.println("Fail to start the ${AppInfo.name} app because:")
|
||||
System.err.println("Fail to start the ${AppInfo.displayName} app because:")
|
||||
e.printStackTrace()
|
||||
exitProcess(-1)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import com.abdownloadmanager.desktop.pages.home.HomeComponent
|
||||
import com.abdownloadmanager.desktop.pages.queue.QueuesComponent
|
||||
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
||||
import com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadComponent
|
||||
import com.abdownloadmanager.desktop.pages.updater.UpdateComponent
|
||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.ui.widget.MessageDialogModel
|
||||
@ -42,6 +43,7 @@ import com.abdownloadmanager.resources.*
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.category.CategoryManager
|
||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||
import com.arkivanov.decompose.childContext
|
||||
import ir.amirab.downloader.exception.TooManyErrorException
|
||||
import ir.amirab.downloader.monitor.isDownloadActiveFlow
|
||||
import ir.amirab.util.compose.StringSource
|
||||
@ -847,8 +849,10 @@ class AppComponent(
|
||||
).all { it }
|
||||
}
|
||||
|
||||
// TODO enable updater
|
||||
// val updater = UpdateComponent(childContext("updater"))
|
||||
val updater = UpdateComponent(
|
||||
childContext("updater"),
|
||||
this,
|
||||
)
|
||||
val showAboutPage = MutableStateFlow(false)
|
||||
val showOpenSourceLibraries = MutableStateFlow(false)
|
||||
val showTranslators = MutableStateFlow(false)
|
||||
|
@ -5,10 +5,13 @@ import com.abdownloadmanager.desktop.utils.BrowserType
|
||||
|
||||
interface BaseConstants{
|
||||
val appName:String
|
||||
val appDisplayName: String
|
||||
val packageName:String
|
||||
val projectWebsite:String
|
||||
val projectSourceCode:String
|
||||
val projectTranslations: String
|
||||
val projectGithubOwner: String
|
||||
val projectGithubRepo: String
|
||||
val browserIntegrations:List<BrowserIntegrationModel>
|
||||
val telegramGroupUrl:String
|
||||
val telegramChannelUrl:String
|
||||
@ -16,10 +19,13 @@ interface BaseConstants{
|
||||
|
||||
object SharedConstants:BaseConstants{
|
||||
override val appName: String = BuildConfig.APP_NAME
|
||||
override val appDisplayName: String = BuildConfig.APP_DISPLAY_NAME
|
||||
override val packageName: String = BuildConfig.PACKAGE_NAME
|
||||
override val projectWebsite: String= BuildConfig.PROJECT_WEBSITE
|
||||
override val projectTranslations: String = BuildConfig.PROJECT_TRANSLATIONS
|
||||
override val projectSourceCode: String= BuildConfig.PROJECT_SOURCE_CODE
|
||||
override val projectGithubOwner: String = BuildConfig.PROJECT_GITHUB_OWNER
|
||||
override val projectGithubRepo: String = BuildConfig.PROJECT_GITHUB_REPO
|
||||
override val browserIntegrations: List<BrowserIntegrationModel> = listOf(
|
||||
BrowserIntegrationModel(
|
||||
BrowserType.Chrome,BuildConfig.INTEGRATION_CHROME_LINK
|
||||
|
@ -149,12 +149,12 @@ val showDownloadList = simpleAction(
|
||||
appComponent.openHome()
|
||||
}
|
||||
|
||||
/*val checkForUpdateAction = simpleAction(
|
||||
title = "Check For Update",
|
||||
val checkForUpdateAction = simpleAction(
|
||||
title = Res.string.update_check_for_update.asStringSource(),
|
||||
icon = MyIcons.refresh,
|
||||
) {
|
||||
appComponent.updater.requestCheckForUpdate()
|
||||
}*/
|
||||
}
|
||||
val openAboutAction = simpleAction(
|
||||
title = Res.string.about.asStringSource(),
|
||||
icon = MyIcons.info,
|
||||
|
@ -1,10 +1,15 @@
|
||||
package com.abdownloadmanager.desktop.di
|
||||
|
||||
import GithubApi
|
||||
import com.abdownloadmanager.UpdateDownloadLocationProvider
|
||||
import com.abdownloadmanager.UpdateManager
|
||||
import com.abdownloadmanager.desktop.AppArguments
|
||||
import com.abdownloadmanager.integration.IntegrationHandler
|
||||
import com.abdownloadmanager.desktop.AppComponent
|
||||
import com.abdownloadmanager.desktop.SharedConstants
|
||||
import com.abdownloadmanager.desktop.integration.IntegrationHandlerImp
|
||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||
import com.abdownloadmanager.desktop.pages.updater.UpdateDownloaderViaDownloadSystem
|
||||
import ir.amirab.downloader.queue.QueueManager
|
||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||
import com.abdownloadmanager.desktop.storage.*
|
||||
@ -23,6 +28,8 @@ import ir.amirab.downloader.monitor.DownloadMonitor
|
||||
import ir.amirab.downloader.utils.IDiskStat
|
||||
import ir.amirab.util.startup.Startup
|
||||
import com.abdownloadmanager.integration.Integration
|
||||
import com.abdownloadmanager.updateapplier.DesktopUpdateApplier
|
||||
import com.abdownloadmanager.updateapplier.UpdateApplier
|
||||
import ir.amirab.downloader.DownloadManager
|
||||
import ir.amirab.util.config.datastore.createMapConfigDatastore
|
||||
import kotlinx.coroutines.*
|
||||
@ -33,12 +40,14 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import com.abdownloadmanager.updatechecker.DummyUpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.GithubUpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||
import com.abdownloadmanager.utils.DownloadFoldersRegistry
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
import com.abdownloadmanager.utils.FileIconProviderUsingCategoryIcons
|
||||
import ir.amirab.util.AppVersionTracker
|
||||
import com.abdownloadmanager.utils.appinfo.PreviousVersion
|
||||
import com.abdownloadmanager.utils.autoremove.RemovedDownloadsFromDiskTracker
|
||||
import com.abdownloadmanager.utils.category.*
|
||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||
@ -188,14 +197,47 @@ val integrationModule = module {
|
||||
}
|
||||
}
|
||||
val updaterModule = module {
|
||||
single {
|
||||
UpdateDownloadLocationProvider {
|
||||
AppInfo.updateDir.resolve("downloads")
|
||||
}
|
||||
}
|
||||
single<UpdateApplier> {
|
||||
DesktopUpdateApplier(
|
||||
installationFolder = AppInfo.installationFolder,
|
||||
updateFolder = AppInfo.updateDir.path,
|
||||
logDir = AppInfo.logDir.path,
|
||||
appName = AppInfo.name,
|
||||
updateDownloader = UpdateDownloaderViaDownloadSystem(
|
||||
get(),
|
||||
get(),
|
||||
),
|
||||
)
|
||||
}
|
||||
single<UpdateChecker> {
|
||||
DummyUpdateChecker(AppVersion.get())
|
||||
GithubUpdateChecker(
|
||||
AppVersion.get(),
|
||||
githubApi = GithubApi(
|
||||
owner = SharedConstants.projectGithubOwner,
|
||||
repo = SharedConstants.projectGithubRepo,
|
||||
client = OkHttpClient
|
||||
.Builder()
|
||||
.build()
|
||||
)
|
||||
)
|
||||
}
|
||||
single {
|
||||
UpdateManager(
|
||||
updateChecker = get(),
|
||||
updateApplier = get(),
|
||||
appVersionTracker = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
val startUpModule = module {
|
||||
single {
|
||||
Startup.getStartUpManagerForDesktop(
|
||||
name = AppInfo.name,
|
||||
name = AppInfo.displayName,
|
||||
path = AppInfo.exeFile,
|
||||
args = listOf(AppArguments.Args.BACKGROUND),
|
||||
)
|
||||
@ -270,6 +312,21 @@ val appModule = module {
|
||||
get(), get(), get(),
|
||||
)
|
||||
}
|
||||
single {
|
||||
PreviousVersion(
|
||||
systemPath = AppInfo.systemDir,
|
||||
currentVersion = AppInfo.version,
|
||||
)
|
||||
}
|
||||
single {
|
||||
AppVersionTracker(
|
||||
previousVersion = {
|
||||
// it MUST be booted first
|
||||
get<PreviousVersion>().get()
|
||||
},
|
||||
currentVersion = AppInfo.version,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -27,11 +27,9 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.App
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.resources.*
|
||||
import ir.amirab.util.compose.resources.myStringResource
|
||||
|
||||
@Composable
|
||||
@ -83,7 +81,7 @@ fun RenderAppInfo(
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
AppInfo.name,
|
||||
AppInfo.displayName,
|
||||
fontSize = myTextSizes.xl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
@ -20,8 +20,10 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.UpdateManager
|
||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import com.abdownloadmanager.utils.FileIconProvider
|
||||
@ -41,10 +43,12 @@ import ir.amirab.util.flow.mapTwoWayStateFlow
|
||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
||||
import ir.amirab.downloader.downloaditem.contexts.User
|
||||
import ir.amirab.util.AppVersionTracker
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
@ -413,6 +417,8 @@ class HomeComponent(
|
||||
private val queueManager: QueueManager by inject()
|
||||
private val pageStorage: PageStatesStorage by inject()
|
||||
private val appSettings: AppSettingsStorage by inject()
|
||||
private val updateManager: UpdateManager by inject()
|
||||
private val appVersionTracker: AppVersionTracker by inject()
|
||||
val filterState = FilterState()
|
||||
val mergeTopBarWithTitleBar = appSettings.mergeTopBarWithTitleBar
|
||||
|
||||
@ -589,8 +595,9 @@ class HomeComponent(
|
||||
+gotoSettingsAction
|
||||
}
|
||||
subMenu(Res.string.help.asStringSource()) {
|
||||
//TODO Enable Updater
|
||||
// +checkForUpdateAction
|
||||
if (updateManager.isUpdateSupported()) {
|
||||
+checkForUpdateAction
|
||||
}
|
||||
+supportActionGroup
|
||||
separator()
|
||||
+openOpenSourceThirdPartyLibraries
|
||||
@ -814,6 +821,33 @@ class HomeComponent(
|
||||
downloads.any { it.id == previouslySelectedItem }
|
||||
}
|
||||
}.launchIn(scope)
|
||||
// if the app is updated then clean downloaded files
|
||||
if (appVersionTracker.isUpgraded()) {
|
||||
// clean update files
|
||||
scope.launch {
|
||||
// temporary fix:
|
||||
// at the moment we relly on DownloadMonitor for getting the list of downloads by their folder
|
||||
// so wait for the download list to be updated by the download monitor
|
||||
delay(1000)
|
||||
// then clean up the downloaded files
|
||||
updateManager.cleanDownloadedFiles()
|
||||
}
|
||||
// show user about update
|
||||
scope.launch {
|
||||
// let user focus to the app
|
||||
delay(1000)
|
||||
notificationSender.sendNotification(
|
||||
title = Res.string.update_updater.asStringSource(),
|
||||
description = Res.string.update_app_updated_to_version_n.asStringSourceWithARgs(
|
||||
Res.string.update_app_updated_to_version_n_createArgs(
|
||||
version = appVersionTracker.currentVersion.toString()
|
||||
)
|
||||
),
|
||||
type = NotificationType.Success,
|
||||
tag = "Updater"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val selectionListItems = combineStateFlows(
|
||||
|
@ -13,9 +13,6 @@ import com.abdownloadmanager.desktop.ui.customwindow.rememberWindowController
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
import com.abdownloadmanager.desktop.utils.AppInfo
|
||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import com.abdownloadmanager.resources.*
|
||||
import ir.amirab.util.compose.resources.myStringResource
|
||||
import java.awt.Dimension
|
||||
|
||||
@Composable
|
||||
@ -31,7 +28,7 @@ fun HomeWindow(
|
||||
val onCloseRequest = onCLoseRequest
|
||||
val windowIcon = MyIcons.appIcon
|
||||
val windowController = rememberWindowController(
|
||||
AppInfo.name,
|
||||
AppInfo.displayName,
|
||||
windowIcon.rememberPainter(),
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.abdownloadmanager.desktop.pages.updater
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.WindowIcon
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
@ -8,69 +9,143 @@ import com.abdownloadmanager.desktop.ui.theme.myTextSizes
|
||||
import com.abdownloadmanager.desktop.ui.widget.ActionButton
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import com.abdownloadmanager.desktop.utils.div
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.ui.theme.myMarkdownColors
|
||||
import com.abdownloadmanager.desktop.ui.theme.myMarkdownTypography
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import com.abdownloadmanager.updatechecker.VersionData
|
||||
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||
import com.abdownloadmanager.utils.compose.needScroll
|
||||
import com.mikepenz.markdown.compose.Markdown
|
||||
import ir.amirab.util.compose.resources.myStringResource
|
||||
|
||||
@Composable
|
||||
fun NewUpdatePage(
|
||||
versionVersionData: VersionData,
|
||||
newVersionInfo: UpdateInfo,
|
||||
currentVersion: Version,
|
||||
update: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
WindowTitle("New Update")
|
||||
WindowIcon(MyIcons.appIcon)
|
||||
WindowTitle(myStringResource(Res.string.update_updater))
|
||||
WindowIcon(MyIcons.refresh)
|
||||
Box {
|
||||
BackgroundEffects()
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
top = 8.dp
|
||||
)
|
||||
.weight(1f)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "There is a new version of app is available",
|
||||
text = myStringResource(Res.string.update_available),
|
||||
fontSize = myTextSizes.xl,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
WithContentAlpha(0.75f){
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "you can press on update button to update to the latest version",
|
||||
fontSize = myTextSizes.base,
|
||||
text = myStringResource(
|
||||
Res.string.version_n, Res.string.version_n_createArgs(
|
||||
newVersionInfo.version.toString()
|
||||
)
|
||||
),
|
||||
fontSize = myTextSizes.xl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = myColors.success,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row {
|
||||
RenderKeyValue("Current Version", currentVersion.toString())
|
||||
Spacer(Modifier.width(16.dp))
|
||||
RenderKeyValue("Latest Version", versionVersionData.version.toString())
|
||||
WithContentAlpha(0.8f) {
|
||||
Text(
|
||||
text = myStringResource(Res.string.update_available_suggest_to_to_update),
|
||||
fontSize = myTextSizes.base,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
RenderChangeLog(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
versionVersionData.changeLog
|
||||
newVersionInfo.changeLog
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(
|
||||
}
|
||||
Actions(
|
||||
Modifier.fillMaxWidth(),
|
||||
update,
|
||||
cancel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.BackgroundEffects() {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.offset(y = (-148).dp)
|
||||
.fillMaxWidth(0.5f)
|
||||
.height(200.dp)
|
||||
.blur(
|
||||
56.dp,
|
||||
edgeTreatment = BlurredEdgeTreatment.Unbounded
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
myColors.primary / 0.15f
|
||||
)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(180.dp)
|
||||
.offset(x = 32.dp, y = (-32).dp)
|
||||
.blur(
|
||||
56.dp,
|
||||
edgeTreatment = BlurredEdgeTreatment.Unbounded
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
myColors.secondary / 0.15f
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Actions(modifier: Modifier, update: () -> Unit, cancel: () -> Unit) {
|
||||
Column(modifier) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
.background(myColors.onBackground / 0.15f)
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(myColors.surface / 0.5f)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
UpdateButton(Modifier, update)
|
||||
@ -99,7 +174,7 @@ fun UpdateButton(
|
||||
}
|
||||
)
|
||||
ActionButton(
|
||||
text = "Update",
|
||||
text = myStringResource(Res.string.update),
|
||||
modifier = modifier,
|
||||
onClick = update,
|
||||
backgroundColor = backgroundColor,
|
||||
@ -115,35 +190,59 @@ fun CancelButton(
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
ActionButton(
|
||||
text = "Cancel",
|
||||
text = myStringResource(Res.string.cancel),
|
||||
modifier = modifier,
|
||||
onClick = cancel,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderChangeLog(modifier: Modifier, changeLog: String) {
|
||||
private fun RenderChangeLog(modifier: Modifier, changeLog: String) {
|
||||
val trimmedChangelog = remember {
|
||||
changeLog
|
||||
.lines()
|
||||
.filterNot { it.isBlank() }
|
||||
.joinToString("\n")
|
||||
}
|
||||
Column(modifier) {
|
||||
Text(
|
||||
text = "Changelog",
|
||||
fontSize = myTextSizes.base,
|
||||
text = myStringResource(Res.string.update_release_notes),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = myTextSizes.lg,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Box(
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollbarAdapter = rememberScrollbarAdapter(scrollState)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(myColors.onBackground / 5)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(8.dp)
|
||||
.clip(shape)
|
||||
.border(1.dp, myColors.onBackground / 0.05f, shape)
|
||||
.background(myColors.surface / 75)
|
||||
) {
|
||||
SelectionContainer {
|
||||
WithContentAlpha(0.75f) {
|
||||
Text(
|
||||
text = changeLog,
|
||||
fontSize = myTextSizes.base,
|
||||
Markdown(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
.padding(8.dp),
|
||||
content = trimmedChangelog,
|
||||
colors = myMarkdownColors(),
|
||||
typography = myMarkdownTypography()
|
||||
)
|
||||
if (scrollbarAdapter.needScroll()) {
|
||||
VerticalScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(
|
||||
vertical = 4.dp,
|
||||
horizontal = 4.dp
|
||||
),
|
||||
style = LocalScrollbarStyle.current.copy(
|
||||
thickness = 8.dp
|
||||
),
|
||||
adapter = scrollbarAdapter
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,9 +255,17 @@ private fun RenderKeyValue(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
WithContentAlpha(0.50f) {
|
||||
Text(key, fontSize = myTextSizes.base)
|
||||
Text(
|
||||
key,
|
||||
fontSize = myTextSizes.base,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(value, fontSize = myTextSizes.base)
|
||||
Text(
|
||||
value,
|
||||
fontSize = myTextSizes.base,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,88 +1,63 @@
|
||||
package com.abdownloadmanager.desktop.pages.updater
|
||||
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.abdownloadmanager.desktop.utils.AppVersion
|
||||
import com.abdownloadmanager.desktop.utils.BaseComponent
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.abdownloadmanager.UpdateManager
|
||||
import com.abdownloadmanager.desktop.NotificationSender
|
||||
import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import com.abdownloadmanager.updateapplier.JavaUpdateApplier
|
||||
import com.abdownloadmanager.updateapplier.UpdateDownloader
|
||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.VersionData
|
||||
import java.io.File
|
||||
|
||||
sealed interface UpdateStatus {
|
||||
data object IDLE : UpdateStatus
|
||||
data object NoUpdate : UpdateStatus
|
||||
data object NewUpdate : UpdateStatus
|
||||
data class Error(val e: Throwable) : UpdateStatus
|
||||
data object Checking : UpdateStatus
|
||||
}
|
||||
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
|
||||
class UpdateComponent(
|
||||
ctx: ComponentContext,
|
||||
) : BaseComponent(
|
||||
ctx
|
||||
),
|
||||
private val notificationSender: NotificationSender,
|
||||
) : BaseComponent(ctx),
|
||||
KoinComponent {
|
||||
private val updateChecker: UpdateChecker by inject()
|
||||
//maybe create it via DI
|
||||
// private val updateApplier: UpdateApplier by inject()
|
||||
|
||||
private val downloadSystem: DownloadSystem by inject()
|
||||
private val updateManager: UpdateManager by inject()
|
||||
|
||||
val currentVersion = AppVersion.get()
|
||||
val showNewUpdate = MutableStateFlow(false)
|
||||
val newVersionData = MutableStateFlow(null as VersionData?)
|
||||
private val appSettings: AppSettingsStorage by inject()
|
||||
val newVersionData = updateManager.newVersionData
|
||||
private var updateApplierJob: Job? = null
|
||||
|
||||
var updateCheckStatus by mutableStateOf<UpdateStatus>(UpdateStatus.IDLE)
|
||||
var updateCheckStatus = updateManager.updateCheckStatus
|
||||
|
||||
fun performUpdate() {
|
||||
val versionData = newVersionData.value ?: error("there is no new version!")
|
||||
val updateApplier = JavaUpdateApplier(
|
||||
versionData,
|
||||
UpdateDownloaderViaDownloadSystem(
|
||||
downloadSystem,
|
||||
appSettings.defaultDownloadFolder.value,
|
||||
name = versionData.name
|
||||
)
|
||||
)
|
||||
updateApplierJob?.cancel()
|
||||
updateApplierJob = scope.launch {
|
||||
updateApplier.applyUpdate()
|
||||
try {
|
||||
updateManager.update()
|
||||
} catch (e: Exception) {
|
||||
showMessage(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showNewUpdate(versionData: VersionData) {
|
||||
newVersionData.update { versionData }
|
||||
private fun showMessage(e: Exception) {
|
||||
e.printStackTrace()
|
||||
notificationSender.sendDialogNotification(
|
||||
"Update Error".asStringSource(),
|
||||
e.localizedMessage.orEmpty().asStringSource(),
|
||||
type = MessageDialogType.Error,
|
||||
)
|
||||
}
|
||||
|
||||
fun showNewUpdate() {
|
||||
showNewUpdate.update { true }
|
||||
}
|
||||
|
||||
fun requestCheckForUpdate() {
|
||||
scope.launch {
|
||||
try {
|
||||
updateCheckStatus = UpdateStatus.Checking
|
||||
val result = updateChecker.check()
|
||||
if (result != null) {
|
||||
showNewUpdate(result)
|
||||
updateCheckStatus = UpdateStatus.NewUpdate
|
||||
} else {
|
||||
updateCheckStatus = UpdateStatus.NoUpdate
|
||||
}
|
||||
updateCheckStatus = UpdateStatus.IDLE
|
||||
}catch (e:Exception){
|
||||
updateCheckStatus = UpdateStatus.Error(e)
|
||||
updateManager
|
||||
.checkForUpdate()
|
||||
?.let {
|
||||
showNewUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,38 +66,3 @@ class UpdateComponent(
|
||||
showNewUpdate.update { false }
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateDownloaderViaDownloadSystem(
|
||||
private val downloadSystem: DownloadSystem,
|
||||
private val saveFolder: String,
|
||||
private val name: String,
|
||||
) : UpdateDownloader,
|
||||
KoinComponent {
|
||||
override suspend fun download(link: String): File {
|
||||
val id = downloadSystem.getOrCreateDownloadByLink(
|
||||
DownloadItem(
|
||||
id = -1,
|
||||
link = link,
|
||||
folder = saveFolder,
|
||||
name = name,
|
||||
)
|
||||
)
|
||||
val downloaded = coroutineScope {
|
||||
val waiter = async {
|
||||
downloadSystem.downloadMonitor.waitForDownloadToFinishOrCancel(id)
|
||||
}
|
||||
downloadSystem.manualResume(id)
|
||||
waiter.await()
|
||||
}
|
||||
if (!downloaded) {
|
||||
error("Download Cancelled")
|
||||
}
|
||||
// we recheck download info maybe some dude change the file name!
|
||||
val downloadedItem = downloadSystem.getDownloadItemById(id)
|
||||
requireNotNull(downloadedItem) {
|
||||
"Download is removed!"
|
||||
}
|
||||
return downloadSystem.getDownloadFile(downloadedItem)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package com.abdownloadmanager.desktop.pages.updater
|
||||
|
||||
import com.abdownloadmanager.UpdateDownloadLocationProvider
|
||||
import com.abdownloadmanager.updateapplier.UpdateDownloader
|
||||
import com.abdownloadmanager.updatechecker.UpdateSource
|
||||
import com.abdownloadmanager.utils.DownloadSystem
|
||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
||||
import ir.amirab.downloader.downloaditem.EmptyContext
|
||||
import ir.amirab.downloader.utils.OnDuplicateStrategy
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.io.File
|
||||
|
||||
class UpdateDownloaderViaDownloadSystem(
|
||||
private val downloadSystem: DownloadSystem,
|
||||
private val updateDownloadLocationProvider: UpdateDownloadLocationProvider,
|
||||
) : UpdateDownloader {
|
||||
override suspend fun downloadUpdate(updateDirectDownloadLink: UpdateSource.DirectDownloadLink): File {
|
||||
val updateDownloadsFolder = updateDownloadLocationProvider.getSaveLocation().path
|
||||
val updateDownloads = downloadSystem.getDownloadItemsByFolder(updateDownloadsFolder)
|
||||
val pausedDownload = updateDownloads.find {
|
||||
it.name == updateDirectDownloadLink.name
|
||||
}
|
||||
// at the moment if the download was finished but removed from the filesystem
|
||||
// download will not be restarted automatically
|
||||
val requireRestartDownload = pausedDownload?.getFullPath()?.exists()?.not() ?: false
|
||||
val id = pausedDownload?.id
|
||||
?: downloadSystem.addDownload(
|
||||
downloadItem = DownloadItem(
|
||||
id = -1,
|
||||
link = updateDirectDownloadLink.link,
|
||||
folder = updateDownloadsFolder,
|
||||
name = updateDirectDownloadLink.name,
|
||||
),
|
||||
onDuplicateStrategy = OnDuplicateStrategy.AddNumbered,
|
||||
queueId = null,
|
||||
categoryId = null,
|
||||
)
|
||||
coroutineScope {
|
||||
if (requireRestartDownload) {
|
||||
downloadSystem.reset(id)
|
||||
}
|
||||
val waiter = async {
|
||||
downloadSystem.downloadMonitor.waitForDownloadToFinishOrCancel(id)
|
||||
}
|
||||
downloadSystem.manualResume(id, EmptyContext)
|
||||
waiter.await()
|
||||
}
|
||||
// we recheck download info maybe some dude change the file name!
|
||||
val downloadedItem = downloadSystem.getDownloadItemById(id)
|
||||
requireNotNull(downloadedItem) {
|
||||
"Download is removed!"
|
||||
}
|
||||
return downloadSystem.getDownloadFile(downloadedItem)
|
||||
}
|
||||
|
||||
override suspend fun removeUpdate(updateDirectDownloadLink: UpdateSource.DirectDownloadLink) {
|
||||
val id = downloadSystem
|
||||
.getDownloadItemsByFolder(updateDownloadLocationProvider.getSaveLocation().path)
|
||||
.find { it.name == updateDirectDownloadLink.name }?.id
|
||||
id?.let {
|
||||
downloadSystem.removeDownload(id, true, EmptyContext)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeAllUpdates() {
|
||||
val ids = downloadSystem
|
||||
.getDownloadItemsByFolder(updateDownloadLocationProvider.getSaveLocation().path)
|
||||
.map { it.id }
|
||||
for (id in ids) {
|
||||
downloadSystem.removeDownload(
|
||||
id = id, alsoRemoveFile = true, EmptyContext
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
package com.abdownloadmanager.desktop.pages.updater
|
||||
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
|
||||
import com.abdownloadmanager.desktop.ui.widget.NotificationModel
|
||||
import com.abdownloadmanager.desktop.ui.widget.NotificationType
|
||||
import com.abdownloadmanager.desktop.ui.widget.ShowNotification
|
||||
import com.abdownloadmanager.desktop.ui.widget.useNotification
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.WindowPosition
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import com.abdownloadmanager.UpdateCheckStatus
|
||||
import com.abdownloadmanager.desktop.ui.theme.LocalUiScale
|
||||
import com.abdownloadmanager.resources.Res
|
||||
import ir.amirab.util.compose.StringSource
|
||||
import ir.amirab.util.compose.asStringSource
|
||||
import ir.amirab.util.desktop.screen.applyUiScale
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@ -21,9 +26,9 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
||||
val closeUpdatePage = {
|
||||
updaterComponent.requestClose()
|
||||
}
|
||||
val status = updaterComponent.updateCheckStatus
|
||||
val status = updaterComponent.updateCheckStatus.collectAsState().value
|
||||
|
||||
var message by remember { mutableStateOf(null as String?) }
|
||||
var message by remember { mutableStateOf(null as StringSource?) }
|
||||
var notificationType by remember { mutableStateOf(null as NotificationType?) }
|
||||
LaunchedEffect(status) {
|
||||
fun CoroutineScope.clearMessageAfter(delay: Long) {
|
||||
@ -33,23 +38,27 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
||||
}
|
||||
}
|
||||
when (status) {
|
||||
UpdateStatus.Checking -> {
|
||||
message = "Checking for update"
|
||||
UpdateCheckStatus.Checking -> {
|
||||
message = Res.string.update_checking_for_update.asStringSource()
|
||||
notificationType = NotificationType.Loading(null)
|
||||
}
|
||||
|
||||
is UpdateStatus.Error -> {
|
||||
is UpdateCheckStatus.Error -> {
|
||||
clearMessageAfter(3000)
|
||||
message = """
|
||||
Error while checking for update
|
||||
${status.e.localizedMessage}
|
||||
""".trimIndent()
|
||||
message = StringSource.CombinedStringSource(
|
||||
listOf(
|
||||
Res.string.update_check_error.asStringSource(),
|
||||
status.e.localizedMessage.orEmpty().asStringSource(),
|
||||
),
|
||||
"\n",
|
||||
)
|
||||
status.e.printStackTrace()
|
||||
notificationType = NotificationType.Error
|
||||
}
|
||||
|
||||
UpdateStatus.NoUpdate -> {
|
||||
UpdateCheckStatus.NoUpdate -> {
|
||||
clearMessageAfter(3000)
|
||||
message = "No update"
|
||||
message = Res.string.update_no_update.asStringSource()
|
||||
notificationType = NotificationType.Info
|
||||
}
|
||||
|
||||
@ -62,21 +71,23 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
||||
|
||||
message?.let { message ->
|
||||
ShowNotification(
|
||||
title = "Updater".asStringSource(),
|
||||
description = message.asStringSource(),
|
||||
title = Res.string.update_updater.asStringSource(),
|
||||
description = message,
|
||||
type = notificationType ?: NotificationType.Info,
|
||||
tag = "Updater"
|
||||
)
|
||||
}
|
||||
if (showUpdate && newVersion != null) {
|
||||
val uiScale = LocalUiScale.current
|
||||
CustomWindow(
|
||||
state = rememberWindowState(
|
||||
size = DpSize(400.dp, 400.dp)
|
||||
size = DpSize(500.dp, 400.dp).applyUiScale(uiScale),
|
||||
position = WindowPosition.Aligned(Alignment.Center)
|
||||
),
|
||||
onCloseRequest = closeUpdatePage,
|
||||
) {
|
||||
NewUpdatePage(
|
||||
versionVersionData = newVersion,
|
||||
newVersionInfo = newVersion,
|
||||
currentVersion = updaterComponent.currentVersion,
|
||||
cancel = closeUpdatePage,
|
||||
update = {
|
||||
|
@ -29,6 +29,7 @@ import com.abdownloadmanager.desktop.pages.credits.translators.ShowTranslators
|
||||
import com.abdownloadmanager.desktop.pages.editdownload.EditDownloadWindow
|
||||
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||
import com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog
|
||||
import com.abdownloadmanager.desktop.ui.widget.*
|
||||
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||
@ -89,8 +90,7 @@ object Ui : KoinComponent {
|
||||
ShowAddDownloadDialogs(appComponent)
|
||||
ShowDownloadDialogs(appComponent)
|
||||
ShowCategoryDialogs(appComponent)
|
||||
//TODO Enable Updater
|
||||
//ShowUpdaterDialog(appComponent.updater)
|
||||
ShowUpdaterDialog(appComponent.updater)
|
||||
ShowAboutDialog(appComponent)
|
||||
NewQueueDialog(appComponent)
|
||||
ShowMessageDialogs(appComponent)
|
||||
|
@ -0,0 +1,86 @@
|
||||
package com.abdownloadmanager.desktop.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import com.abdownloadmanager.utils.compose.LocalContentColor
|
||||
import com.abdownloadmanager.utils.compose.LocalTextStyle
|
||||
import com.mikepenz.markdown.model.DefaultMarkdownColors
|
||||
import com.mikepenz.markdown.model.DefaultMarkdownTypography
|
||||
|
||||
@Composable
|
||||
fun myMarkdownColors(): DefaultMarkdownColors {
|
||||
return DefaultMarkdownColors(
|
||||
text = LocalContentColor.current,
|
||||
linkText = myColors.info,
|
||||
codeText = myColors.onSurface,
|
||||
inlineCodeText = myColors.onSurface,
|
||||
codeBackground = myColors.surface,
|
||||
dividerColor = LocalContentColor.current.copy(alpha = 0.1f),
|
||||
inlineCodeBackground = myColors.surface,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun myMarkdownTypography(): DefaultMarkdownTypography {
|
||||
val defaultTextStyle = LocalTextStyle.current
|
||||
return DefaultMarkdownTypography(
|
||||
h1 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.xl * 1.1f,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
h2 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.xl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
h3 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.lg,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
h4 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
h5 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.sm,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
h6 = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.xs,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
text = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
code = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
inlineCode = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
),
|
||||
quote = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
),
|
||||
paragraph = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
),
|
||||
ordered = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
),
|
||||
bullet = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
),
|
||||
list = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
fontWeight = FontWeight.Normal,
|
||||
),
|
||||
link = defaultTextStyle.copy(
|
||||
fontSize = myTextSizes.base,
|
||||
),
|
||||
)
|
||||
}
|
@ -7,6 +7,7 @@ import java.io.File
|
||||
|
||||
object AppInfo {
|
||||
val name = SharedConstants.appName
|
||||
val displayName = SharedConstants.appDisplayName
|
||||
val packageName = SharedConstants.packageName
|
||||
val website = SharedConstants.projectWebsite
|
||||
val sourceCode = SharedConstants.projectSourceCode
|
||||
@ -21,6 +22,18 @@ object AppInfo {
|
||||
// }
|
||||
System.getProperty("jpackage.app-path")
|
||||
}
|
||||
val installationFolder: String? = run {
|
||||
exeFile?.let(::File)
|
||||
?.parentFile // executable path
|
||||
?.let {
|
||||
when (Platform.getCurrentPlatform()) {
|
||||
Platform.Desktop.Linux -> it.parentFile // <installationFolder>/bin/ABDownloadManager
|
||||
Platform.Desktop.MacOS -> it.parentFile // not checked yet
|
||||
Platform.Desktop.Windows -> it // <installationFolder>/ABDownloadManager.exe
|
||||
else -> null
|
||||
}?.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AppInfo.isAppInstalled(): Boolean {
|
||||
@ -36,6 +49,9 @@ fun AppInfo.isInDebugMode(): Boolean {
|
||||
}
|
||||
|
||||
val AppInfo.configDir: File get() = File(AppProperties.getConfigDirectory())
|
||||
val AppInfo.systemDir: File get() = File(AppProperties.getSystemDirectory())
|
||||
val AppInfo.updateDir: File get() = AppInfo.systemDir.resolve("update")
|
||||
val AppInfo.logDir: File get() = AppInfo.systemDir.resolve("log")
|
||||
val AppInfo.optionsDir: File get() = AppInfo.configDir.resolve("options")
|
||||
val AppInfo.downloadDbDir: File get() = AppInfo.configDir.resolve("download_db")
|
||||
fun AppInfo.extensions() {
|
||||
|
@ -20,6 +20,7 @@ object AppProperties {
|
||||
|
||||
private object Keys {
|
||||
const val CONFIG_DIRECTORY: String = "app.config.path"
|
||||
const val SYSTEM_DIRECTORY: String = "app.system.path"
|
||||
const val DEBUG: String = "app.debug"
|
||||
}
|
||||
|
||||
@ -81,6 +82,11 @@ object AppProperties {
|
||||
return ensureAndGet(Keys.CONFIG_DIRECTORY)
|
||||
.toString()
|
||||
}
|
||||
|
||||
fun getSystemDirectory(): String {
|
||||
return ensureAndGet(Keys.SYSTEM_DIRECTORY)
|
||||
.toString()
|
||||
}
|
||||
//app.properties in installation directory
|
||||
fun isAppPropertiesFound(): Boolean {
|
||||
return foundAppProperties
|
||||
|
@ -4,8 +4,6 @@ import com.abdownloadmanager.desktop.utils.AppInfo
|
||||
import com.abdownloadmanager.desktop.utils.isAppInstalled
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
data class NativeMessagingManifests(
|
||||
val firefoxNativeMessagingManifest: FirefoxNativeMessagingManifest,
|
||||
@ -65,8 +63,8 @@ class NativeMessaging(
|
||||
if (!AppInfo.isAppInstalled()) return null
|
||||
val execFile = AppInfo.exeFile!!
|
||||
return FirefoxNativeMessagingManifest(
|
||||
name = AppInfo.name,
|
||||
description = AppInfo.name,
|
||||
name = AppInfo.displayName,
|
||||
description = AppInfo.displayName,
|
||||
path = execFile,
|
||||
type = "stdio",
|
||||
allowedExtensions = listOf(
|
||||
@ -78,8 +76,8 @@ class NativeMessaging(
|
||||
if (!AppInfo.isAppInstalled()) return null
|
||||
val execFile = AppInfo.exeFile!!
|
||||
return ChromeNativeMessagingManifest(
|
||||
name = AppInfo.name,
|
||||
description = AppInfo.name,
|
||||
name = AppInfo.displayName,
|
||||
description = AppInfo.displayName,
|
||||
path = execFile,
|
||||
type = "stdio",
|
||||
allowedOrigins = listOf(
|
||||
|
@ -1,2 +1,3 @@
|
||||
app.config.path="${user.home}/.abdm/config"
|
||||
app.system.path="${user.home}/.abdm/system"
|
||||
app.debug="false"
|
||||
|
@ -260,8 +260,8 @@ class DownloadMonitor(
|
||||
)
|
||||
|
||||
override suspend fun waitForDownloadToFinishOrCancel(
|
||||
id: Long
|
||||
): Boolean {
|
||||
id: Long,
|
||||
) {
|
||||
val event = downloadManager
|
||||
.listOfJobsEvents
|
||||
.filter {
|
||||
@ -278,10 +278,8 @@ class DownloadMonitor(
|
||||
is DownloadManagerEvents.OnJobStarting -> false
|
||||
}
|
||||
}
|
||||
if (event is DownloadManagerEvents.OnJobCompleted) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
if (event is DownloadManagerEvents.OnJobCanceled) {
|
||||
throw event.e
|
||||
}
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ interface IDownloadMonitor {
|
||||
val activeDownloadCount: StateFlow<Int>
|
||||
|
||||
suspend fun waitForDownloadToFinishOrCancel(
|
||||
id: Long
|
||||
): Boolean
|
||||
id: Long,
|
||||
)
|
||||
}
|
||||
|
||||
fun IDownloadMonitor.isDownloadActiveFlow(
|
||||
|
@ -26,6 +26,7 @@ semver = "2.0.0"
|
||||
jgit = "6.9.0.202403050737-r"
|
||||
osThemeDetector = "3.9.1"
|
||||
kotlinFileWatcher = "1.3.0"
|
||||
markdownRenderer = "0.27.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
@ -117,7 +118,7 @@ arrow-opticKsp = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref =
|
||||
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
osThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "osThemeDetector" }
|
||||
|
||||
markdownRenderer-core = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
|
||||
handlebarsJava = "com.github.jknack:handlebars:4.4.0"
|
||||
|
||||
kotlinFileWatcher = { module = "io.github.irgaly.kfswatch:kfswatch", version.ref = "kotlinFileWatcher" }
|
||||
|
@ -126,10 +126,12 @@ class DownloadSystem(
|
||||
}
|
||||
|
||||
suspend fun manualResume(id: Long): Boolean {
|
||||
// if (mainDownloadQueue.isQueueActive) {
|
||||
// return false
|
||||
// }
|
||||
downloadManager.resume(id, ResumedBy(User))
|
||||
manualResume(id, ResumedBy(User))
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun manualResume(id: Long, context: DownloadItemContext): Boolean {
|
||||
downloadManager.resume(id, context)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -211,6 +213,11 @@ class DownloadSystem(
|
||||
it.getFullPath().path == path
|
||||
}
|
||||
}
|
||||
fun getDownloadItemsByFolder(folder: String): List<IDownloadItemState> {
|
||||
return downloadMonitor.downloadListFlow.value.filter {
|
||||
it.folder == folder
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun getFilePathById(id: Long): File? {
|
||||
|
@ -0,0 +1,29 @@
|
||||
package com.abdownloadmanager.utils.appinfo
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import java.io.File
|
||||
|
||||
class PreviousVersion(
|
||||
systemPath: File,
|
||||
private val currentVersion: Version,
|
||||
) {
|
||||
private val versionFile = File(systemPath, ".version")
|
||||
private var previousVersion: Version? = null
|
||||
fun get(): Version? {
|
||||
return previousVersion
|
||||
}
|
||||
|
||||
fun boot() {
|
||||
previousVersion = kotlin.runCatching {
|
||||
// maybe versionFile is null but we catch it
|
||||
val versionString = versionFile.readText()
|
||||
Version.parse(versionString)
|
||||
}.getOrNull()
|
||||
kotlin.runCatching {
|
||||
versionFile.parentFile.mkdirs()
|
||||
versionFile.writeText(currentVersion.toString())
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.abdownloadmanager.utils.compose
|
||||
|
||||
fun androidx.compose.foundation.v2.ScrollbarAdapter.needScroll(): Boolean {
|
||||
return contentSize > viewportSize
|
||||
}
|
@ -316,3 +316,13 @@ meet_the_translators=Meet the Translators
|
||||
localized_by_translators=Localized by Translators
|
||||
confirm_exit=Confirm Exit
|
||||
confirm_exit_description=Are you sure you want to exit AB Download Manager?\nActive downloads/queues will be stopped!
|
||||
update=Update
|
||||
update_updater=Updater
|
||||
update_available=Update Available
|
||||
update_available_suggest_to_to_update=You can update to the latest version to enjoy new features, enhancements, and performance improvements.
|
||||
update_release_notes=Release Notes
|
||||
update_check_for_update=Check for Update
|
||||
update_checking_for_update=Checking for Update
|
||||
update_no_update=You are using the latest version
|
||||
update_check_error=Error while checking for update
|
||||
update_app_updated_to_version_n=App updated to version {{version}}
|
@ -7,4 +7,6 @@ dependencies {
|
||||
api(libs.okhttp.okhttp)
|
||||
api(libs.kotlin.coroutines.core)
|
||||
implementation(project(":shared:utils"))
|
||||
implementation(libs.jna.platform)
|
||||
implementation(libs.semver)
|
||||
}
|
@ -1,44 +1,37 @@
|
||||
package com.abdownloadmanager
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Arch
|
||||
import ir.amirab.util.platform.Platform
|
||||
|
||||
data class AppArtifactInfo(
|
||||
val version: Version,
|
||||
val platform: Platform,
|
||||
val arch: Arch,
|
||||
)
|
||||
|
||||
object ArtifactUtil {
|
||||
private val versionPatern = "(\\d+\\.\\d+\\.\\d+)"
|
||||
val versionRegex = "_$versionPatern".toRegex()
|
||||
val platformRegex = "_${versionPatern}_([a-zA-Z]+)".toRegex()
|
||||
fun extractVersion(name: String): Version? {
|
||||
versionRegex.toString()
|
||||
val versionString = versionRegex.find(name)?.groupValues?.get(1) ?: return null
|
||||
return Version.parse(versionString)
|
||||
}
|
||||
|
||||
fun extractVersionFromTag(tagName: String): Version? {
|
||||
return versionRegex.find(tagName)?.value?.let {
|
||||
Version.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractPlatformFromName(name: String): Platform? {
|
||||
val platformString = platformRegex.find(name)?.groupValues?.get(2) ?: return null
|
||||
return Platform.fromString(platformString)
|
||||
}
|
||||
|
||||
val artifactRegex =
|
||||
"(?<appName>[a-zA-Z]+)_(?<version>(\\d+\\.\\d+\\.\\d+))_(?<platform>[a-zA-Z]+)_(?<arch>[a-zA-Z0-9]+)\\.(?<extension>.+)".toRegex()
|
||||
|
||||
fun getArtifactInfo(name: String): AppArtifactInfo? {
|
||||
val version = extractVersion(name) ?: return null
|
||||
val platform = extractPlatformFromName(name) ?: return null
|
||||
val values = artifactRegex.find(name)?.groups ?: return null
|
||||
val version = runCatching { values.get("version")?.value }
|
||||
.getOrNull()
|
||||
?.let(Version::parse)
|
||||
?: return null
|
||||
val platform = runCatching { values.get("platform")?.value }
|
||||
.getOrNull()
|
||||
?.let(Platform::fromString)
|
||||
?: return null
|
||||
val arch = runCatching { values.get("arch")?.value }
|
||||
.getOrNull()
|
||||
?.let(Arch::fromString)
|
||||
?: return null
|
||||
return AppArtifactInfo(
|
||||
version = version,
|
||||
platform = platform,
|
||||
arch = arch,
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.abdownloadmanager
|
||||
|
||||
import java.io.File
|
||||
|
||||
fun interface UpdateDownloadLocationProvider {
|
||||
fun getSaveLocation(): File
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.abdownloadmanager
|
||||
|
||||
import com.abdownloadmanager.updateapplier.UpdateApplier
|
||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||
import ir.amirab.util.AppVersionTracker
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
sealed interface UpdateCheckStatus {
|
||||
data object IDLE : UpdateCheckStatus
|
||||
data object NoUpdate : UpdateCheckStatus
|
||||
data object NewUpdate : UpdateCheckStatus
|
||||
data class Error(val e: Throwable) : UpdateCheckStatus
|
||||
data object Checking : UpdateCheckStatus
|
||||
}
|
||||
|
||||
class UpdateManager(
|
||||
private val updateChecker: UpdateChecker,
|
||||
private val updateApplier: UpdateApplier,
|
||||
private val appVersionTracker: AppVersionTracker,
|
||||
) {
|
||||
private var _newVersionData: MutableStateFlow<UpdateInfo?> = MutableStateFlow(null)
|
||||
val newVersionData = _newVersionData.asStateFlow()
|
||||
private val _updateCheckStatus: MutableStateFlow<UpdateCheckStatus> = MutableStateFlow(UpdateCheckStatus.IDLE)
|
||||
val updateCheckStatus = _updateCheckStatus.asStateFlow()
|
||||
suspend fun cleanDownloadedFiles() {
|
||||
kotlin.runCatching {
|
||||
updateApplier.cleanup()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun isUpdateSupported(): Boolean {
|
||||
return updateApplier.updateSupported()
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): UpdateInfo? {
|
||||
val newUpdateCheck = try {
|
||||
_updateCheckStatus.update { UpdateCheckStatus.Checking }
|
||||
val checkedData = updateChecker.check()
|
||||
_updateCheckStatus.value = if (checkedData == null) {
|
||||
UpdateCheckStatus.NoUpdate
|
||||
} else {
|
||||
UpdateCheckStatus.NewUpdate
|
||||
}
|
||||
checkedData
|
||||
} catch (e: Exception) {
|
||||
_updateCheckStatus.update { UpdateCheckStatus.Error(e) }
|
||||
null
|
||||
}
|
||||
_newVersionData.update { newUpdateCheck }
|
||||
return newUpdateCheck
|
||||
}
|
||||
|
||||
suspend fun update() {
|
||||
_newVersionData.value?.let {
|
||||
if (updateApplier.updateSupported()) {
|
||||
updateApplier.applyUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add onAfter update installed
|
||||
// ...
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.abdownloadmanager.updateapplier;
|
||||
|
||||
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||
import com.abdownloadmanager.updatechecker.UpdateSource
|
||||
import ir.amirab.util.platform.Platform
|
||||
import java.io.File
|
||||
|
||||
class DesktopUpdateApplier(
|
||||
private val installationFolder: String?,
|
||||
private val appName: String,
|
||||
private val updateFolder: String,
|
||||
private val logDir: String,
|
||||
private val updateDownloader: UpdateDownloader,
|
||||
) : UpdateApplier {
|
||||
private var downloading: Boolean = false
|
||||
override fun updateSupported(): Boolean {
|
||||
val installationFolder = installationFolder ?: return false
|
||||
return File(installationFolder).canWrite()
|
||||
}
|
||||
|
||||
private fun isAppInstalledWithNSIS(): Boolean {
|
||||
return File(installationFolder, "uninstall.exe").exists()
|
||||
}
|
||||
|
||||
private fun extension(name: String): String {
|
||||
return name.substringAfterLast('.', "")
|
||||
}
|
||||
|
||||
private fun isArchiveFile(name: String): Boolean {
|
||||
return name.endsWith(".tar.gz") || name.endsWith(".zip")
|
||||
}
|
||||
|
||||
private fun isExeFile(name: String): Boolean {
|
||||
return name.endsWith(".exe") || name.endsWith(".zip")
|
||||
}
|
||||
|
||||
override suspend fun applyUpdate(
|
||||
updateInfo: UpdateInfo,
|
||||
) {
|
||||
if (!updateSupported()) {
|
||||
return
|
||||
}
|
||||
val installationFolder = requireNotNull(installationFolder) {
|
||||
"update applier can only apply update if installation folder is not null"
|
||||
}
|
||||
//it is only check for same instance
|
||||
// if I faced to multiple update (when user press "update" many times)
|
||||
// I have to cancel this suspension job and create a new instance instead
|
||||
if (downloading) {
|
||||
return
|
||||
}
|
||||
downloading = true
|
||||
val downloadableSources = updateInfo.updateSource.filterIsInstance<UpdateSource.DirectDownloadLink>()
|
||||
var downloadSource = downloadableSources.find {
|
||||
isArchiveFile(it.name)
|
||||
}
|
||||
if (Platform.getCurrentPlatform() == Platform.Desktop.Windows) {
|
||||
val exeDirectDownloadLink = downloadableSources.find {
|
||||
isExeFile(it.name)
|
||||
}
|
||||
if (isAppInstalledWithNSIS() && exeDirectDownloadLink != null) {
|
||||
downloadSource = exeDirectDownloadLink
|
||||
}
|
||||
}
|
||||
requireNotNull(downloadSource) {
|
||||
"Can't find proper download link for your platform! Please update it manually"
|
||||
}
|
||||
val downloadedFile = updateDownloader.downloadUpdate(downloadSource)
|
||||
if (!downloadedFile.exists()) {
|
||||
downloading = false
|
||||
return
|
||||
}
|
||||
val updateInstaller = when {
|
||||
isArchiveFile(downloadSource.name) -> {
|
||||
UpdateInstallerFromArchiveFile(
|
||||
archiveFile = downloadedFile,
|
||||
installationFolder = installationFolder,
|
||||
appFolderInArchive = appName,
|
||||
folderToExtractUpdate = File(updateFolder).resolve("extracted"),
|
||||
logDir = logDir,
|
||||
)
|
||||
}
|
||||
|
||||
isExeFile(downloadedFile.name) -> {
|
||||
UpdateInstallerByWindowsExecutable(downloadedFile)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// should not happen btw
|
||||
error("can't install ${extension(downloadSource.name)} format automatically! please update it manually!")
|
||||
}
|
||||
}
|
||||
// updateDownloader.removeUpdate(updateInfo)
|
||||
try {
|
||||
updateInstaller.installUpdate()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(
|
||||
buildString {
|
||||
appendLine("can't start installation")
|
||||
e.localizedMessage?.let(this::append)
|
||||
},
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
override suspend fun cleanup() {
|
||||
updateDownloader.removeAllUpdates()
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package com.abdownloadmanager.updateapplier;
|
||||
|
||||
import ir.amirab.util.osfileutil.FileUtils
|
||||
import com.abdownloadmanager.updatechecker.VersionData
|
||||
import java.io.File
|
||||
interface UpdateDownloader{
|
||||
suspend fun download(link:String):File
|
||||
}
|
||||
class JavaUpdateApplier(
|
||||
private val versionData: VersionData,
|
||||
private val updateDownloader: UpdateDownloader
|
||||
) : UpdateApplier() {
|
||||
private var downloading: Boolean = false
|
||||
|
||||
override suspend fun applyUpdate() {
|
||||
//it is only check for same instance
|
||||
// if I faced to multiple update (when user press "update" many times)
|
||||
// I have to cancel this suspension job and create a new instance instead
|
||||
if (downloading){
|
||||
return
|
||||
}
|
||||
downloading=true
|
||||
|
||||
val executableFile = updateDownloader.download(versionData.link)
|
||||
if (!executableFile.exists() || !executableFile.canExecute()){
|
||||
downloading=false
|
||||
return
|
||||
}
|
||||
//TODO investigate on possible security risks
|
||||
FileUtils.openFile(executableFile)
|
||||
}
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
package com.abdownloadmanager.updateapplier
|
||||
|
||||
abstract class UpdateApplier{
|
||||
abstract suspend fun applyUpdate()
|
||||
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||
|
||||
interface UpdateApplier {
|
||||
fun updateSupported(): Boolean
|
||||
suspend fun applyUpdate(updateInfo: UpdateInfo)
|
||||
suspend fun cleanup()
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.abdownloadmanager.updateapplier
|
||||
|
||||
import com.abdownloadmanager.updatechecker.UpdateSource
|
||||
import java.io.File
|
||||
|
||||
interface UpdateDownloader {
|
||||
suspend fun downloadUpdate(updateDirectDownloadLink: UpdateSource.DirectDownloadLink): File
|
||||
suspend fun removeUpdate(updateDirectDownloadLink: UpdateSource.DirectDownloadLink)
|
||||
suspend fun removeAllUpdates()
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.abdownloadmanager.updateapplier
|
||||
|
||||
interface UpdateInstaller {
|
||||
fun installUpdate()
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
package com.abdownloadmanager.updateapplier
|
||||
|
||||
import java.io.File
|
||||
|
||||
class UpdateInstallerByWindowsExecutable(
|
||||
private val executable: File,
|
||||
) : UpdateInstaller {
|
||||
override fun installUpdate() {
|
||||
val file = executable.absolutePath
|
||||
ProcessBuilder()
|
||||
.command("cmd", "/c", file, "/S")
|
||||
.start()
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package com.abdownloadmanager.updateapplier
|
||||
|
||||
import ir.amirab.util.platform.Platform
|
||||
import okio.FileSystem
|
||||
import okio.Path.Companion.toPath
|
||||
import okio.buffer
|
||||
import okio.use
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* the duty of the script is
|
||||
* it accepts [folderToExtractUpdate], [installationFolder]
|
||||
* 1. stop the app
|
||||
* 2. remove the installed app files
|
||||
* 3. copy [folderToExtractUpdate] into [installationFolder]
|
||||
* 4. remove [folderToExtractUpdate]
|
||||
* 5. start the app again
|
||||
*/
|
||||
class UpdateInstallerFromArchiveFile(
|
||||
private val archiveFile: File,
|
||||
private val installationFolder: String,
|
||||
private val folderToExtractUpdate: File,
|
||||
private val appFolderInArchive: String,
|
||||
private val logDir: String,
|
||||
) : UpdateInstaller {
|
||||
private fun getScriptPath(logFile: String): String {
|
||||
val platform = Platform.getCurrentPlatform()
|
||||
val scriptForPlatform = when (platform) {
|
||||
Platform.Desktop.Linux -> {
|
||||
"com/abdownloadmanager/updater/updater_linux.sh"
|
||||
}
|
||||
|
||||
Platform.Desktop.Windows -> {
|
||||
"com/abdownloadmanager/updater/updater_windows.bat"
|
||||
}
|
||||
|
||||
else -> error("script for this platform not found")
|
||||
}.toPath()
|
||||
extractTo(archiveFile, folderToExtractUpdate)
|
||||
val updateFolder = folderToExtractUpdate.resolve(appFolderInArchive)
|
||||
require(updateFolder.exists()) {
|
||||
"Can't find required files for this update please update it manually"
|
||||
}
|
||||
val scriptExtension = scriptForPlatform.toString().substringAfterLast('.', "")
|
||||
val scriptContent = FileSystem.RESOURCES.source(scriptForPlatform).buffer().use {
|
||||
it.readUtf8()
|
||||
}
|
||||
val scriptPathInTempFolder = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(
|
||||
"abdm-updater.$scriptExtension"
|
||||
)
|
||||
scriptPathInTempFolder.toFile().writeText(scriptContent)
|
||||
val scriptContentFile = scriptPathInTempFolder.toString()
|
||||
val commandToRun = when (platform) {
|
||||
Platform.Desktop.Linux -> execInBash(
|
||||
scriptPath = scriptContentFile,
|
||||
updateFolder = updateFolder.path,
|
||||
installationFolder = installationFolder,
|
||||
logFile = logFile,
|
||||
)
|
||||
|
||||
Platform.Desktop.MacOS -> execInBash(
|
||||
scriptPath = scriptContentFile,
|
||||
updateFolder = updateFolder.path,
|
||||
installationFolder = installationFolder,
|
||||
logFile = logFile,
|
||||
)
|
||||
|
||||
Platform.Desktop.Windows -> execInCMD(
|
||||
scriptPath = scriptContentFile,
|
||||
updateFolder = updateFolder.path,
|
||||
installationFolder = installationFolder,
|
||||
logFile = logFile,
|
||||
)
|
||||
|
||||
else -> error("platform ${platform} not supported")
|
||||
}
|
||||
val scriptToRun = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve("abdm-updater.run.$scriptExtension")
|
||||
scriptToRun.toFile().writeText(commandToRun)
|
||||
return scriptToRun.toString()
|
||||
}
|
||||
|
||||
private fun executeScript() {
|
||||
val logFile = File(logDir, "update_log.txt")
|
||||
.apply {
|
||||
parentFile.mkdirs()
|
||||
}.path
|
||||
val scriptPath = getScriptPath(logFile)
|
||||
|
||||
|
||||
val command = when (val p = Platform.getCurrentPlatform()) {
|
||||
Platform.Desktop.Linux -> arrayOf("bash", scriptPath)
|
||||
Platform.Desktop.MacOS -> arrayOf("bash", scriptPath)
|
||||
Platform.Desktop.Windows -> arrayOf("cmd", "/c", scriptPath)
|
||||
else -> error("platform: $p not supported for updating by script")
|
||||
}
|
||||
// println("execute script $command")
|
||||
ProcessBuilder()
|
||||
.command(*command)
|
||||
.apply {
|
||||
// in linux if I don't remove it the program won't restart
|
||||
environment().remove("_JPACKAGE_LAUNCHER")
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun execInCMD(
|
||||
scriptPath: String,
|
||||
updateFolder: String,
|
||||
installationFolder: String,
|
||||
logFile: String,
|
||||
): String {
|
||||
return """
|
||||
cmd /c ""${scriptPath}" "${updateFolder}" "${installationFolder}" > "${logFile}" 2>&1"
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun execInBash(
|
||||
scriptPath: String,
|
||||
updateFolder: String,
|
||||
installationFolder: String,
|
||||
logFile: String,
|
||||
): String {
|
||||
return """
|
||||
bash "${scriptPath}" "${updateFolder}" "${installationFolder}" > "${logFile}" 2>&1 &
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
override fun installUpdate() {
|
||||
executeScript()
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractTo(archiveFile: File, destinationFolder: File) {
|
||||
val name = archiveFile.name
|
||||
require(!destinationFolder.isFile) {
|
||||
"destination folder is a file!"
|
||||
}
|
||||
destinationFolder.mkdirs()
|
||||
require(destinationFolder.isDirectory) {
|
||||
"destination folder is not created!"
|
||||
}
|
||||
when {
|
||||
name.endsWith(".zip") -> extractZip(archiveFile, destinationFolder)
|
||||
name.endsWith("tar.gz") -> extractTarGzUsingTar(archiveFile, destinationFolder)
|
||||
else -> error("archive file not detected for this file name: $name")
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractZip(zipFile: File, outputDirPath: File) {
|
||||
ZipInputStream(zipFile.inputStream()).use { zis ->
|
||||
var entry: ZipEntry? = null
|
||||
while (true) {
|
||||
entry = zis.nextEntry
|
||||
if (entry == null) break
|
||||
val outputFile = outputDirPath.resolve(entry.name)
|
||||
if (entry.isDirectory) {
|
||||
outputFile.mkdirs()
|
||||
} else {
|
||||
outputFile.parentFile.mkdirs()
|
||||
outputFile.outputStream().use { fileOutputStream ->
|
||||
zis.copyTo(fileOutputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractTarGzUsingTar(tarGzFilePath: File, outputDirPath: File) {
|
||||
val tarCommand = listOf("tar", "-xzvf", tarGzFilePath.path, "-C", outputDirPath.path)
|
||||
try {
|
||||
val process = ProcessBuilder(tarCommand)
|
||||
.start()
|
||||
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode == 0) {
|
||||
println("Extraction completed successfully.")
|
||||
} else {
|
||||
println("Error during extraction. Exit code: $exitCode")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Failed to execute tar command: ${e.message}")
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.abdownloadmanager.updatechecker
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Arch
|
||||
import ir.amirab.util.platform.Platform
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class DummyUpdateChecker(currentVersion: Version) : UpdateChecker(currentVersion) {
|
||||
override suspend fun getMyPlatformLatestVersion(): UpdateInfo {
|
||||
val newVersion = currentVersion.copy(
|
||||
major = currentVersion.minor + 1,
|
||||
preRelease = null,
|
||||
buildMetadata = null,
|
||||
)
|
||||
delay(1000)
|
||||
// error("Something wrong")
|
||||
return UpdateInfo(
|
||||
version = newVersion,
|
||||
platform = Platform.getCurrentPlatform(),
|
||||
arch = Arch.getCurrentArch(),
|
||||
updateSource = listOf(
|
||||
UpdateSource.DirectDownloadLink(
|
||||
link = "http://127.0.0.1:8080/ABDownloadManager_1.4.4_windows_x64.zip",
|
||||
name = "ABDownloadManager_1.4.4_windows_x64.zip",
|
||||
hash = "md5:0123456789abcdef",
|
||||
)
|
||||
),
|
||||
changeLog = """
|
||||
1. there is an improve on download engine.
|
||||
2. fix known bugs.
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.abdownloadmanager.updatechecker
|
||||
|
||||
import GithubApi
|
||||
import com.abdownloadmanager.ArtifactUtil
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Arch
|
||||
import ir.amirab.util.platform.Platform
|
||||
|
||||
class GithubUpdateChecker(
|
||||
currentVersion: Version,
|
||||
private val githubApi: GithubApi,
|
||||
) : UpdateChecker(currentVersion) {
|
||||
override suspend fun getMyPlatformLatestVersion(): UpdateInfo {
|
||||
return getLatestVersionsForThisDevice()
|
||||
}
|
||||
|
||||
private suspend fun getLatestVersionsForThisDevice(): UpdateInfo {
|
||||
val release = githubApi.getLatestReleases()
|
||||
val currentPlatform = Platform.getCurrentPlatform()
|
||||
val currentArch = Arch.getCurrentArch()
|
||||
val updateSources = mutableListOf<UpdateSource>()
|
||||
var foundVersion: Version? = null
|
||||
var initializedVersionFromAssetNames = false
|
||||
for (asset in release.assets) {
|
||||
val v = ArtifactUtil.getArtifactInfo(asset.name) ?: continue
|
||||
if (v.platform != currentPlatform) continue
|
||||
if (v.arch != currentArch) continue
|
||||
if (!initializedVersionFromAssetNames) {
|
||||
foundVersion = v.version
|
||||
initializedVersionFromAssetNames = true
|
||||
}
|
||||
val isHashFile = asset.name.endsWith(".md5")
|
||||
if (isHashFile) {
|
||||
// nothing for now!
|
||||
} else {
|
||||
updateSources.add(
|
||||
UpdateSource.DirectDownloadLink(
|
||||
asset.downloadLink,
|
||||
asset.name,
|
||||
null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return UpdateInfo(
|
||||
version = foundVersion
|
||||
?: Version.parse(release.version.substring("v".length)),
|
||||
platform = currentPlatform,
|
||||
arch = currentArch,
|
||||
changeLog = release.body ?: "",
|
||||
updateSource = updateSources
|
||||
)
|
||||
}
|
||||
}
|
@ -1,64 +1,15 @@
|
||||
package com.abdownloadmanager.updatechecker
|
||||
|
||||
import GithubApi
|
||||
import com.abdownloadmanager.ArtifactUtil
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Platform
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
private class GithubUpdateChecker(
|
||||
currentVersion: Version,
|
||||
val githubApi: GithubApi,
|
||||
) : UpdateChecker(currentVersion) {
|
||||
override suspend fun getMyPlatformLatestVersion(): VersionData {
|
||||
val all=getLatestVersions()
|
||||
val versionData = all.find { it.platform == Platform.getCurrentPlatform() }
|
||||
return requireNotNull(versionData){
|
||||
"could not find latest version for current platform"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLatestVersions(): List<VersionData> {
|
||||
val release = githubApi.getLatestReleases()
|
||||
return release.assets.mapNotNull {
|
||||
val v = ArtifactUtil.getArtifactInfo(it.name) ?: return@mapNotNull null
|
||||
VersionData(
|
||||
name = it.name,
|
||||
version = v.version,
|
||||
link = it.downloadLink,
|
||||
platform = v.platform,
|
||||
changeLog = release.body?:""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DummyUpdateChecker(currentVersion :Version): UpdateChecker(currentVersion ){
|
||||
override suspend fun getMyPlatformLatestVersion(): VersionData {
|
||||
val newVersion=currentVersion.copy(major = currentVersion.major+1)
|
||||
delay(5000)
|
||||
error("Something wrong")
|
||||
return VersionData(
|
||||
version = newVersion,
|
||||
platform = Platform.getCurrentPlatform(),
|
||||
link = "http://localhost:3000/app_1.0.1_windows.msi",
|
||||
name = "app_1.0.1_windows.msi",
|
||||
changeLog = """
|
||||
1. there is an improve on download engine.
|
||||
2. fix known bugs.
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class UpdateChecker(
|
||||
protected val currentVersion: Version,
|
||||
) {
|
||||
abstract suspend fun getMyPlatformLatestVersion(): VersionData
|
||||
suspend fun check(): VersionData? {
|
||||
abstract suspend fun getMyPlatformLatestVersion(): UpdateInfo
|
||||
suspend fun check(): UpdateInfo? {
|
||||
val latest = getMyPlatformLatestVersion()
|
||||
requireNotNull(latest){ "There is no release for this platform" }
|
||||
require(latest.updateSource.isNotEmpty()) { "There is no release for this platform" }
|
||||
return latest.takeIf {
|
||||
it.version > currentVersion
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package com.abdownloadmanager.updatechecker
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Arch
|
||||
import ir.amirab.util.platform.Platform
|
||||
|
||||
data class UpdateInfo(
|
||||
val version: Version,
|
||||
val platform: Platform,
|
||||
val arch: Arch,
|
||||
val updateSource: List<UpdateSource>,
|
||||
val changeLog: String,
|
||||
)
|
||||
|
||||
sealed interface UpdateSource {
|
||||
data class DirectDownloadLink(
|
||||
val link: String,
|
||||
val name: String,
|
||||
val hash: String?,
|
||||
) : UpdateSource
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package com.abdownloadmanager.updatechecker
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
import ir.amirab.util.platform.Platform
|
||||
|
||||
data class VersionData(
|
||||
val version: Version,
|
||||
val platform: Platform,
|
||||
val name:String,
|
||||
val link: String,
|
||||
val changeLog:String,
|
||||
)
|
@ -0,0 +1,86 @@
|
||||
APP_NAME="ABDownloadManager"
|
||||
awaitTermination(){
|
||||
local processName="${1:?}"
|
||||
local count=0
|
||||
while true; do
|
||||
local pids=$(pidof "$processName")
|
||||
if [ -z "$pids" ]; then
|
||||
break
|
||||
fi
|
||||
if [ $count -eq 10 ]; then
|
||||
echo "timeout waiting for $processName to terminate"
|
||||
break
|
||||
fi
|
||||
echo "waiting for $processName to terminate"
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
stopApp(){
|
||||
echo "stopping the app"
|
||||
local pids=$(pidof "$APP_NAME")
|
||||
if [ -z "$pids" ]; then
|
||||
echo "no process found with name $APP_NAME"
|
||||
return
|
||||
fi
|
||||
kill -9 "$pids"
|
||||
awaitTermination "$APP_NAME"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed to stop $APP_NAME"
|
||||
return 1
|
||||
fi
|
||||
echo "process $APP_NAME stopped"
|
||||
}
|
||||
removeCurrentInstallation(){
|
||||
local installationFolder="${1:?}"
|
||||
filesToRemove=(
|
||||
"bin"
|
||||
"lib"
|
||||
)
|
||||
echo "removing current installation"
|
||||
for filesToRemove in "${filesToRemove[@]}" ; do
|
||||
echo "executing rm -rf \"$installationFolder/$filesToRemove\""
|
||||
rm -rf "$installationFolder/$filesToRemove"
|
||||
done
|
||||
}
|
||||
copyUpdateToInstallationFolder(){
|
||||
local updateFile="$1"
|
||||
local installationFolder="${2:?"installationFolder not passed"}"
|
||||
echo "copying update files to installation folder"
|
||||
echo "executing: cp -a \"$updateFile/.\" $installationFolder"
|
||||
cp -a "$updateFile/." "$installationFolder"
|
||||
}
|
||||
|
||||
removeUpdateFiles(){
|
||||
local updateFile="$1"
|
||||
echo "removing update folder"
|
||||
echo "executing: rm -rf \"$updateFile\""
|
||||
rm -rf "$updateFile"
|
||||
}
|
||||
executablePath(){
|
||||
local installationFolder="${1:?}"
|
||||
echo "$installationFolder/bin/$APP_NAME"
|
||||
}
|
||||
executeProgram(){
|
||||
local installationFolder=$1
|
||||
local path=$(executablePath "$installationFolder")
|
||||
echo "starting $APP_NAME..."
|
||||
echo "executing: \"$path\""
|
||||
"$path"
|
||||
}
|
||||
main(){
|
||||
local updateFile="$1"
|
||||
local installationFolder="$2"
|
||||
|
||||
stopApp "$installationFolder"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "returning back to program"
|
||||
executeProgram "$installationFolder"
|
||||
exit 1
|
||||
fi
|
||||
removeCurrentInstallation "$installationFolder"
|
||||
copyUpdateToInstallationFolder "$updateFile" "$installationFolder"
|
||||
removeUpdateFiles "$updateFile"
|
||||
executeProgram "$installationFolder"
|
||||
}
|
||||
|
||||
main "$@"
|
@ -0,0 +1,82 @@
|
||||
@echo off
|
||||
|
||||
set APP_NAME=ABDownloadManager
|
||||
call :main "%1" "%2"
|
||||
goto :eof
|
||||
|
||||
:stopApp
|
||||
echo execute: taskkill /IM %APP_NAME%.exe /F
|
||||
taskkill /IM %APP_NAME%.exe /F
|
||||
call :wait_for_termination
|
||||
echo %APP_NAME% is terminated
|
||||
goto :eof
|
||||
|
||||
:wait_for_termination
|
||||
echo checking for termination of %APP_NAME%
|
||||
tasklist /FI "IMAGENAME eq %APP_NAME%.exe" | find /I "%APP_NAME%.exe" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
goto :eof
|
||||
) else (
|
||||
ping 127.0.0.1 -n 2 >nul 2>&1
|
||||
goto wait_for_termination
|
||||
)
|
||||
|
||||
|
||||
:removeCurrentInstallation
|
||||
setlocal
|
||||
set installationFolder=%~1
|
||||
set filesToRemove=("app" "runtime" "ABDownloadManager.exe" "ABDownloadManager.ico")
|
||||
for %%f in %filesToRemove% do (
|
||||
if exist %installationFolder%\%%f (
|
||||
if exist %installationFolder%\%%f\* (
|
||||
echo executing rmdir /S /Q %installationFolder%\%%f
|
||||
rmdir /S /Q "%installationFolder%\%%f"
|
||||
) else (
|
||||
echo executing del /F /Q %installationFolder%\%%f
|
||||
del /F /Q "%installationFolder%\%%f"
|
||||
)
|
||||
)
|
||||
)
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:copyUpdateToInstallationFolder
|
||||
setlocal
|
||||
set updateFile=%1
|
||||
set installationFolder=%2
|
||||
echo executing: xcopy /E /I /Y %updateFile% %installationFolder%
|
||||
xcopy /E /I /Y %updateFile% %installationFolder%
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:removeUpdateFolder
|
||||
setlocal
|
||||
set updateFolder=%1
|
||||
echo executing rmdir /S /Q "%updateFolder%"
|
||||
rmdir /S /Q "%updateFolder%"
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:executeProgram
|
||||
setlocal
|
||||
set installationFolder=%~1
|
||||
set code=%2
|
||||
set message=%3
|
||||
echo executing %installationFolder%\%APP_NAME%.exe
|
||||
start "" %installationFolder%\%APP_NAME%.exe
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
:main
|
||||
setlocal
|
||||
set updateFile=%1
|
||||
set installationFolder=%2
|
||||
call :stopApp
|
||||
call :removeCurrentInstallation %installationFolder%
|
||||
call :copyUpdateToInstallationFolder %updateFile% %installationFolder%
|
||||
call :removeUpdateFolder %updateFile%
|
||||
call :executeProgram %installationFolder%
|
||||
endlocal
|
||||
goto :eof
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
package ir.amirab.util
|
||||
|
||||
import io.github.z4kn4fein.semver.Version
|
||||
|
||||
class AppVersionTracker(
|
||||
val previousVersion: () -> Version?,
|
||||
val currentVersion: Version,
|
||||
) {
|
||||
fun isNewInstall(): Boolean {
|
||||
return previousVersion() == null
|
||||
}
|
||||
|
||||
fun isUpgraded(): Boolean {
|
||||
val previousVersion = previousVersion() ?: return false
|
||||
return previousVersion < currentVersion
|
||||
}
|
||||
|
||||
fun isDowngraded(): Boolean {
|
||||
val previousVersion = previousVersion() ?: return false
|
||||
return previousVersion > currentVersion
|
||||
}
|
||||
|
||||
fun isNewOrUpdated() = isNewInstall() || isUpgraded()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user