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.androidx.datastore)
|
||||||
|
|
||||||
implementation(libs.aboutLibraries.core)
|
implementation(libs.aboutLibraries.core)
|
||||||
|
implementation(libs.markdownRenderer.core)
|
||||||
implementation(libs.composeFileKit) {
|
implementation(libs.composeFileKit) {
|
||||||
exclude(group = "net.java.dev.jna")
|
exclude(group = "net.java.dev.jna")
|
||||||
}
|
}
|
||||||
@ -180,13 +180,17 @@ buildConfig {
|
|||||||
getApplicationPackageName()
|
getApplicationPackageName()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"APP_DISPLAY_NAME",
|
||||||
|
provider { getPrettifiedAppName() }
|
||||||
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"APP_VERSION",
|
"APP_VERSION",
|
||||||
provider { getAppVersionString() }
|
provider { getAppVersionString() }
|
||||||
)
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"APP_NAME",
|
"APP_NAME",
|
||||||
provider { getPrettifiedAppName() }
|
provider { getAppName() }
|
||||||
)
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"PROJECT_WEBSITE",
|
"PROJECT_WEBSITE",
|
||||||
@ -200,6 +204,18 @@ buildConfig {
|
|||||||
"https://github.com/amir1376/ab-download-manager"
|
"https://github.com/amir1376/ab-download-manager"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"PROJECT_GITHUB_OWNER",
|
||||||
|
provider {
|
||||||
|
"amir1376"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
buildConfigField(
|
||||||
|
"PROJECT_GITHUB_REPO",
|
||||||
|
provider {
|
||||||
|
"ab-download-manager"
|
||||||
|
}
|
||||||
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"PROJECT_TRANSLATIONS",
|
"PROJECT_TRANSLATIONS",
|
||||||
provider {
|
provider {
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
app.config.path="${user.home}/.abdm/config"
|
app.config.path="${user.home}/.abdm/config"
|
||||||
|
app.system.path="${user.home}/.abdm/system"
|
||||||
app.debug="false"
|
app.debug="false"
|
||||||
|
@ -156,7 +156,13 @@ FunctionEnd
|
|||||||
Delete "$DESKTOP\${APP_DISPLAY_NAME}.lnk"
|
Delete "$DESKTOP\${APP_DISPLAY_NAME}.lnk"
|
||||||
!macroend
|
!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}"
|
Section "${APP_DISPLAY_NAME}"
|
||||||
SectionInstType RO
|
SectionInstType RO
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.abdownloadmanager.desktop
|
package com.abdownloadmanager.desktop
|
||||||
|
|
||||||
|
import com.abdownloadmanager.UpdateManager
|
||||||
import com.abdownloadmanager.desktop.di.Di
|
import com.abdownloadmanager.desktop.di.Di
|
||||||
import com.abdownloadmanager.desktop.ui.Ui
|
import com.abdownloadmanager.desktop.ui.Ui
|
||||||
import com.abdownloadmanager.desktop.utils.*
|
import com.abdownloadmanager.desktop.utils.*
|
||||||
import com.abdownloadmanager.desktop.utils.singleInstance.*
|
import com.abdownloadmanager.desktop.utils.singleInstance.*
|
||||||
import com.abdownloadmanager.integration.Integration
|
import com.abdownloadmanager.integration.Integration
|
||||||
import com.abdownloadmanager.utils.DownloadSystem
|
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 ir.amirab.util.platform.Platform
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
@ -22,6 +23,8 @@ class App : AutoCloseable,
|
|||||||
KoinComponent {
|
KoinComponent {
|
||||||
private val downloadSystem: DownloadSystem by inject()
|
private val downloadSystem: DownloadSystem by inject()
|
||||||
private val integration: Integration 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
|
//TODO Setup Native Messaging Feature
|
||||||
//private val browserNativeMessaging: NativeMessaging by inject()
|
//private val browserNativeMessaging: NativeMessaging by inject()
|
||||||
@ -34,8 +37,10 @@ class App : AutoCloseable,
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
//make sure to not get any dependency until boot the DI Container
|
//make sure to not get any dependency until boot the DI Container
|
||||||
Di.boot()
|
Di.boot()
|
||||||
|
// it's better to organize these list of boot functions in a separate class
|
||||||
integration.boot()
|
integration.boot()
|
||||||
downloadSystem.boot()
|
downloadSystem.boot()
|
||||||
|
previousVersion.boot()
|
||||||
//TODO Setup Native Messaging Feature
|
//TODO Setup Native Messaging Feature
|
||||||
//waiting for compose kmp to add multi launcher to nativeDistributions,the PR is already exists but not merger
|
//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
|
//or maybe I should use a custom solution
|
||||||
@ -79,7 +84,7 @@ fun main(args: Array<String>) {
|
|||||||
appArguments = appArguments,
|
appArguments = appArguments,
|
||||||
)
|
)
|
||||||
} catch (e: Throwable) {
|
} 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()
|
e.printStackTrace()
|
||||||
exitProcess(-1)
|
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.queue.QueuesComponent
|
||||||
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
import com.abdownloadmanager.desktop.pages.settings.SettingsComponent
|
||||||
import com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadComponent
|
import com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadComponent
|
||||||
|
import com.abdownloadmanager.desktop.pages.updater.UpdateComponent
|
||||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||||
import com.abdownloadmanager.desktop.ui.widget.MessageDialogModel
|
import com.abdownloadmanager.desktop.ui.widget.MessageDialogModel
|
||||||
@ -42,6 +43,7 @@ import com.abdownloadmanager.resources.*
|
|||||||
import com.abdownloadmanager.utils.DownloadSystem
|
import com.abdownloadmanager.utils.DownloadSystem
|
||||||
import com.abdownloadmanager.utils.category.CategoryManager
|
import com.abdownloadmanager.utils.category.CategoryManager
|
||||||
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
import com.abdownloadmanager.utils.category.CategorySelectionMode
|
||||||
|
import com.arkivanov.decompose.childContext
|
||||||
import ir.amirab.downloader.exception.TooManyErrorException
|
import ir.amirab.downloader.exception.TooManyErrorException
|
||||||
import ir.amirab.downloader.monitor.isDownloadActiveFlow
|
import ir.amirab.downloader.monitor.isDownloadActiveFlow
|
||||||
import ir.amirab.util.compose.StringSource
|
import ir.amirab.util.compose.StringSource
|
||||||
@ -847,8 +849,10 @@ class AppComponent(
|
|||||||
).all { it }
|
).all { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO enable updater
|
val updater = UpdateComponent(
|
||||||
// val updater = UpdateComponent(childContext("updater"))
|
childContext("updater"),
|
||||||
|
this,
|
||||||
|
)
|
||||||
val showAboutPage = MutableStateFlow(false)
|
val showAboutPage = MutableStateFlow(false)
|
||||||
val showOpenSourceLibraries = MutableStateFlow(false)
|
val showOpenSourceLibraries = MutableStateFlow(false)
|
||||||
val showTranslators = MutableStateFlow(false)
|
val showTranslators = MutableStateFlow(false)
|
||||||
|
@ -5,10 +5,13 @@ import com.abdownloadmanager.desktop.utils.BrowserType
|
|||||||
|
|
||||||
interface BaseConstants{
|
interface BaseConstants{
|
||||||
val appName:String
|
val appName:String
|
||||||
|
val appDisplayName: String
|
||||||
val packageName:String
|
val packageName:String
|
||||||
val projectWebsite:String
|
val projectWebsite:String
|
||||||
val projectSourceCode:String
|
val projectSourceCode:String
|
||||||
val projectTranslations: String
|
val projectTranslations: String
|
||||||
|
val projectGithubOwner: String
|
||||||
|
val projectGithubRepo: String
|
||||||
val browserIntegrations:List<BrowserIntegrationModel>
|
val browserIntegrations:List<BrowserIntegrationModel>
|
||||||
val telegramGroupUrl:String
|
val telegramGroupUrl:String
|
||||||
val telegramChannelUrl:String
|
val telegramChannelUrl:String
|
||||||
@ -16,10 +19,13 @@ interface BaseConstants{
|
|||||||
|
|
||||||
object SharedConstants:BaseConstants{
|
object SharedConstants:BaseConstants{
|
||||||
override val appName: String = BuildConfig.APP_NAME
|
override val appName: String = BuildConfig.APP_NAME
|
||||||
|
override val appDisplayName: String = BuildConfig.APP_DISPLAY_NAME
|
||||||
override val packageName: String = BuildConfig.PACKAGE_NAME
|
override val packageName: String = BuildConfig.PACKAGE_NAME
|
||||||
override val projectWebsite: String= BuildConfig.PROJECT_WEBSITE
|
override val projectWebsite: String= BuildConfig.PROJECT_WEBSITE
|
||||||
override val projectTranslations: String = BuildConfig.PROJECT_TRANSLATIONS
|
override val projectTranslations: String = BuildConfig.PROJECT_TRANSLATIONS
|
||||||
override val projectSourceCode: String= BuildConfig.PROJECT_SOURCE_CODE
|
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(
|
override val browserIntegrations: List<BrowserIntegrationModel> = listOf(
|
||||||
BrowserIntegrationModel(
|
BrowserIntegrationModel(
|
||||||
BrowserType.Chrome,BuildConfig.INTEGRATION_CHROME_LINK
|
BrowserType.Chrome,BuildConfig.INTEGRATION_CHROME_LINK
|
||||||
|
@ -149,12 +149,12 @@ val showDownloadList = simpleAction(
|
|||||||
appComponent.openHome()
|
appComponent.openHome()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*val checkForUpdateAction = simpleAction(
|
val checkForUpdateAction = simpleAction(
|
||||||
title = "Check For Update",
|
title = Res.string.update_check_for_update.asStringSource(),
|
||||||
icon = MyIcons.refresh,
|
icon = MyIcons.refresh,
|
||||||
) {
|
) {
|
||||||
appComponent.updater.requestCheckForUpdate()
|
appComponent.updater.requestCheckForUpdate()
|
||||||
}*/
|
}
|
||||||
val openAboutAction = simpleAction(
|
val openAboutAction = simpleAction(
|
||||||
title = Res.string.about.asStringSource(),
|
title = Res.string.about.asStringSource(),
|
||||||
icon = MyIcons.info,
|
icon = MyIcons.info,
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
package com.abdownloadmanager.desktop.di
|
package com.abdownloadmanager.desktop.di
|
||||||
|
|
||||||
|
import GithubApi
|
||||||
|
import com.abdownloadmanager.UpdateDownloadLocationProvider
|
||||||
|
import com.abdownloadmanager.UpdateManager
|
||||||
import com.abdownloadmanager.desktop.AppArguments
|
import com.abdownloadmanager.desktop.AppArguments
|
||||||
import com.abdownloadmanager.integration.IntegrationHandler
|
import com.abdownloadmanager.integration.IntegrationHandler
|
||||||
import com.abdownloadmanager.desktop.AppComponent
|
import com.abdownloadmanager.desktop.AppComponent
|
||||||
|
import com.abdownloadmanager.desktop.SharedConstants
|
||||||
import com.abdownloadmanager.desktop.integration.IntegrationHandlerImp
|
import com.abdownloadmanager.desktop.integration.IntegrationHandlerImp
|
||||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||||
|
import com.abdownloadmanager.desktop.pages.updater.UpdateDownloaderViaDownloadSystem
|
||||||
import ir.amirab.downloader.queue.QueueManager
|
import ir.amirab.downloader.queue.QueueManager
|
||||||
import com.abdownloadmanager.desktop.repository.AppRepository
|
import com.abdownloadmanager.desktop.repository.AppRepository
|
||||||
import com.abdownloadmanager.desktop.storage.*
|
import com.abdownloadmanager.desktop.storage.*
|
||||||
@ -23,6 +28,8 @@ import ir.amirab.downloader.monitor.DownloadMonitor
|
|||||||
import ir.amirab.downloader.utils.IDiskStat
|
import ir.amirab.downloader.utils.IDiskStat
|
||||||
import ir.amirab.util.startup.Startup
|
import ir.amirab.util.startup.Startup
|
||||||
import com.abdownloadmanager.integration.Integration
|
import com.abdownloadmanager.integration.Integration
|
||||||
|
import com.abdownloadmanager.updateapplier.DesktopUpdateApplier
|
||||||
|
import com.abdownloadmanager.updateapplier.UpdateApplier
|
||||||
import ir.amirab.downloader.DownloadManager
|
import ir.amirab.downloader.DownloadManager
|
||||||
import ir.amirab.util.config.datastore.createMapConfigDatastore
|
import ir.amirab.util.config.datastore.createMapConfigDatastore
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@ -33,12 +40,14 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import com.abdownloadmanager.updatechecker.DummyUpdateChecker
|
import com.abdownloadmanager.updatechecker.GithubUpdateChecker
|
||||||
import com.abdownloadmanager.updatechecker.UpdateChecker
|
import com.abdownloadmanager.updatechecker.UpdateChecker
|
||||||
import com.abdownloadmanager.utils.DownloadFoldersRegistry
|
import com.abdownloadmanager.utils.DownloadFoldersRegistry
|
||||||
import com.abdownloadmanager.utils.DownloadSystem
|
import com.abdownloadmanager.utils.DownloadSystem
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
import com.abdownloadmanager.utils.FileIconProviderUsingCategoryIcons
|
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.autoremove.RemovedDownloadsFromDiskTracker
|
||||||
import com.abdownloadmanager.utils.category.*
|
import com.abdownloadmanager.utils.category.*
|
||||||
import com.abdownloadmanager.utils.compose.IMyIcons
|
import com.abdownloadmanager.utils.compose.IMyIcons
|
||||||
@ -188,14 +197,47 @@ val integrationModule = module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val updaterModule = 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> {
|
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 {
|
val startUpModule = module {
|
||||||
single {
|
single {
|
||||||
Startup.getStartUpManagerForDesktop(
|
Startup.getStartUpManagerForDesktop(
|
||||||
name = AppInfo.name,
|
name = AppInfo.displayName,
|
||||||
path = AppInfo.exeFile,
|
path = AppInfo.exeFile,
|
||||||
args = listOf(AppArguments.Args.BACKGROUND),
|
args = listOf(AppArguments.Args.BACKGROUND),
|
||||||
)
|
)
|
||||||
@ -270,6 +312,21 @@ val appModule = module {
|
|||||||
get(), get(), get(),
|
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.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.abdownloadmanager.desktop.App
|
|
||||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||||
import com.abdownloadmanager.desktop.ui.util.ifThen
|
import com.abdownloadmanager.desktop.ui.util.ifThen
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.resources.*
|
|
||||||
import ir.amirab.util.compose.resources.myStringResource
|
import ir.amirab.util.compose.resources.myStringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -83,7 +81,7 @@ fun RenderAppInfo(
|
|||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
AppInfo.name,
|
AppInfo.displayName,
|
||||||
fontSize = myTextSizes.xl,
|
fontSize = myTextSizes.xl,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
|
@ -20,8 +20,10 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.abdownloadmanager.UpdateManager
|
||||||
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
import com.abdownloadmanager.desktop.pages.category.CategoryDialogManager
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||||
|
import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
|
||||||
import com.abdownloadmanager.resources.Res
|
import com.abdownloadmanager.resources.Res
|
||||||
import com.abdownloadmanager.utils.DownloadSystem
|
import com.abdownloadmanager.utils.DownloadSystem
|
||||||
import com.abdownloadmanager.utils.FileIconProvider
|
import com.abdownloadmanager.utils.FileIconProvider
|
||||||
@ -41,10 +43,12 @@ import ir.amirab.util.flow.mapTwoWayStateFlow
|
|||||||
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
import com.abdownloadmanager.utils.extractors.linkextractor.DownloadCredentialFromStringExtractor
|
||||||
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
import ir.amirab.downloader.downloaditem.contexts.RemovedBy
|
||||||
import ir.amirab.downloader.downloaditem.contexts.User
|
import ir.amirab.downloader.downloaditem.contexts.User
|
||||||
|
import ir.amirab.util.AppVersionTracker
|
||||||
import ir.amirab.util.compose.asStringSource
|
import ir.amirab.util.compose.asStringSource
|
||||||
import ir.amirab.util.compose.asStringSourceWithARgs
|
import ir.amirab.util.compose.asStringSourceWithARgs
|
||||||
import ir.amirab.util.osfileutil.FileUtils
|
import ir.amirab.util.osfileutil.FileUtils
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
@ -413,6 +417,8 @@ class HomeComponent(
|
|||||||
private val queueManager: QueueManager by inject()
|
private val queueManager: QueueManager by inject()
|
||||||
private val pageStorage: PageStatesStorage by inject()
|
private val pageStorage: PageStatesStorage by inject()
|
||||||
private val appSettings: AppSettingsStorage by inject()
|
private val appSettings: AppSettingsStorage by inject()
|
||||||
|
private val updateManager: UpdateManager by inject()
|
||||||
|
private val appVersionTracker: AppVersionTracker by inject()
|
||||||
val filterState = FilterState()
|
val filterState = FilterState()
|
||||||
val mergeTopBarWithTitleBar = appSettings.mergeTopBarWithTitleBar
|
val mergeTopBarWithTitleBar = appSettings.mergeTopBarWithTitleBar
|
||||||
|
|
||||||
@ -589,8 +595,9 @@ class HomeComponent(
|
|||||||
+gotoSettingsAction
|
+gotoSettingsAction
|
||||||
}
|
}
|
||||||
subMenu(Res.string.help.asStringSource()) {
|
subMenu(Res.string.help.asStringSource()) {
|
||||||
//TODO Enable Updater
|
if (updateManager.isUpdateSupported()) {
|
||||||
// +checkForUpdateAction
|
+checkForUpdateAction
|
||||||
|
}
|
||||||
+supportActionGroup
|
+supportActionGroup
|
||||||
separator()
|
separator()
|
||||||
+openOpenSourceThirdPartyLibraries
|
+openOpenSourceThirdPartyLibraries
|
||||||
@ -814,6 +821,33 @@ class HomeComponent(
|
|||||||
downloads.any { it.id == previouslySelectedItem }
|
downloads.any { it.id == previouslySelectedItem }
|
||||||
}
|
}
|
||||||
}.launchIn(scope)
|
}.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(
|
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.ui.icon.MyIcons
|
||||||
import com.abdownloadmanager.desktop.utils.AppInfo
|
import com.abdownloadmanager.desktop.utils.AppInfo
|
||||||
import com.abdownloadmanager.desktop.utils.mvi.HandleEffects
|
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
|
import java.awt.Dimension
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -31,7 +28,7 @@ fun HomeWindow(
|
|||||||
val onCloseRequest = onCLoseRequest
|
val onCloseRequest = onCLoseRequest
|
||||||
val windowIcon = MyIcons.appIcon
|
val windowIcon = MyIcons.appIcon
|
||||||
val windowController = rememberWindowController(
|
val windowController = rememberWindowController(
|
||||||
AppInfo.name,
|
AppInfo.displayName,
|
||||||
windowIcon.rememberPainter(),
|
windowIcon.rememberPainter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.abdownloadmanager.desktop.pages.updater
|
package com.abdownloadmanager.desktop.pages.updater
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
import com.abdownloadmanager.desktop.ui.customwindow.WindowIcon
|
import com.abdownloadmanager.desktop.ui.customwindow.WindowIcon
|
||||||
import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle
|
import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle
|
||||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
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.desktop.ui.widget.ActionButton
|
||||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||||
import com.abdownloadmanager.desktop.utils.div
|
import com.abdownloadmanager.desktop.utils.div
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import com.abdownloadmanager.desktop.ui.widget.Text
|
import com.abdownloadmanager.desktop.ui.widget.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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 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
|
@Composable
|
||||||
fun NewUpdatePage(
|
fun NewUpdatePage(
|
||||||
versionVersionData: VersionData,
|
newVersionInfo: UpdateInfo,
|
||||||
currentVersion: Version,
|
currentVersion: Version,
|
||||||
update: () -> Unit,
|
update: () -> Unit,
|
||||||
cancel: () -> Unit,
|
cancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
WindowTitle("New Update")
|
WindowTitle(myStringResource(Res.string.update_updater))
|
||||||
WindowIcon(MyIcons.appIcon)
|
WindowIcon(MyIcons.refresh)
|
||||||
Column(
|
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 = myStringResource(Res.string.update_available),
|
||||||
|
fontSize = myTextSizes.xl,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
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))
|
||||||
|
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),
|
||||||
|
newVersionInfo.changeLog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Actions(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
update,
|
||||||
|
cancel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.BackgroundEffects() {
|
||||||
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.align(Alignment.TopCenter)
|
||||||
.padding(horizontal = 16.dp)
|
.offset(y = (-148).dp)
|
||||||
.padding(
|
.fillMaxWidth(0.5f)
|
||||||
bottom = 16.dp,
|
.height(200.dp)
|
||||||
top = 8.dp
|
.blur(
|
||||||
|
56.dp,
|
||||||
|
edgeTreatment = BlurredEdgeTreatment.Unbounded
|
||||||
)
|
)
|
||||||
) {
|
.clip(CircleShape)
|
||||||
Text(
|
.background(
|
||||||
text = "There is a new version of app is available",
|
myColors.primary / 0.15f
|
||||||
fontSize = myTextSizes.xl,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
WithContentAlpha(0.75f){
|
|
||||||
Text(
|
|
||||||
text = "you can press on update button to update to the latest version",
|
|
||||||
fontSize = myTextSizes.base,
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Box(
|
||||||
Row {
|
Modifier
|
||||||
RenderKeyValue("Current Version", currentVersion.toString())
|
.align(Alignment.BottomEnd)
|
||||||
Spacer(Modifier.width(16.dp))
|
.size(180.dp)
|
||||||
RenderKeyValue("Latest Version", versionVersionData.version.toString())
|
.offset(x = 32.dp, y = (-32).dp)
|
||||||
}
|
.blur(
|
||||||
Spacer(Modifier.height(8.dp))
|
56.dp,
|
||||||
RenderChangeLog(
|
edgeTreatment = BlurredEdgeTreatment.Unbounded
|
||||||
|
)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
myColors.secondary / 0.15f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Actions(modifier: Modifier, update: () -> Unit, cancel: () -> Unit) {
|
||||||
|
Column(modifier) {
|
||||||
|
Spacer(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.height(1.dp)
|
||||||
versionVersionData.changeLog
|
.background(myColors.onBackground / 0.15f)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(myColors.surface / 0.5f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
UpdateButton(Modifier, update)
|
UpdateButton(Modifier, update)
|
||||||
@ -99,7 +174,7 @@ fun UpdateButton(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Update",
|
text = myStringResource(Res.string.update),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = update,
|
onClick = update,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
@ -115,35 +190,59 @@ fun CancelButton(
|
|||||||
cancel: () -> Unit,
|
cancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ActionButton(
|
ActionButton(
|
||||||
text = "Cancel",
|
text = myStringResource(Res.string.cancel),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = cancel,
|
onClick = cancel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
Column(modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = "Changelog",
|
text = myStringResource(Res.string.update_release_notes),
|
||||||
fontSize = myTextSizes.base,
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = myTextSizes.lg,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Box(
|
val shape = RoundedCornerShape(6.dp)
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val scrollbarAdapter = rememberScrollbarAdapter(scrollState)
|
||||||
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(shape)
|
||||||
.background(myColors.onBackground / 5)
|
.border(1.dp, myColors.onBackground / 0.05f, shape)
|
||||||
.verticalScroll(rememberScrollState())
|
.background(myColors.surface / 75)
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
) {
|
||||||
SelectionContainer {
|
Markdown(
|
||||||
WithContentAlpha(0.75f) {
|
modifier = Modifier
|
||||||
Text(
|
.weight(1f)
|
||||||
text = changeLog,
|
.verticalScroll(scrollState)
|
||||||
fontSize = myTextSizes.base,
|
.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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
WithContentAlpha(0.50f) {
|
WithContentAlpha(0.50f) {
|
||||||
Text(key, fontSize = myTextSizes.base)
|
Text(
|
||||||
|
key,
|
||||||
|
fontSize = myTextSizes.base,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(value, fontSize = myTextSizes.base)
|
Text(
|
||||||
|
value,
|
||||||
|
fontSize = myTextSizes.base,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,89 +1,64 @@
|
|||||||
package com.abdownloadmanager.desktop.pages.updater
|
package com.abdownloadmanager.desktop.pages.updater
|
||||||
|
|
||||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
|
||||||
import com.abdownloadmanager.desktop.utils.AppVersion
|
import com.abdownloadmanager.desktop.utils.AppVersion
|
||||||
import com.abdownloadmanager.desktop.utils.BaseComponent
|
import com.abdownloadmanager.desktop.utils.BaseComponent
|
||||||
import com.abdownloadmanager.utils.DownloadSystem
|
import com.abdownloadmanager.UpdateManager
|
||||||
import androidx.compose.runtime.getValue
|
import com.abdownloadmanager.desktop.NotificationSender
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import com.abdownloadmanager.desktop.ui.widget.MessageDialogType
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import ir.amirab.downloader.downloaditem.DownloadItem
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import com.abdownloadmanager.updateapplier.JavaUpdateApplier
|
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||||
import com.abdownloadmanager.updateapplier.UpdateDownloader
|
import ir.amirab.util.compose.asStringSource
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
class UpdateComponent(
|
class UpdateComponent(
|
||||||
ctx: ComponentContext,
|
ctx: ComponentContext,
|
||||||
) : BaseComponent(
|
private val notificationSender: NotificationSender,
|
||||||
ctx
|
) : BaseComponent(ctx),
|
||||||
),
|
|
||||||
KoinComponent {
|
KoinComponent {
|
||||||
private val updateChecker: UpdateChecker by inject()
|
private val updateManager: UpdateManager by inject()
|
||||||
//maybe create it via DI
|
|
||||||
// private val updateApplier: UpdateApplier by inject()
|
|
||||||
|
|
||||||
private val downloadSystem: DownloadSystem by inject()
|
|
||||||
|
|
||||||
val currentVersion = AppVersion.get()
|
val currentVersion = AppVersion.get()
|
||||||
val showNewUpdate = MutableStateFlow(false)
|
val showNewUpdate = MutableStateFlow(false)
|
||||||
val newVersionData = MutableStateFlow(null as VersionData?)
|
val newVersionData = updateManager.newVersionData
|
||||||
private val appSettings: AppSettingsStorage by inject()
|
|
||||||
private var updateApplierJob: Job? = null
|
private var updateApplierJob: Job? = null
|
||||||
|
|
||||||
var updateCheckStatus by mutableStateOf<UpdateStatus>(UpdateStatus.IDLE)
|
var updateCheckStatus = updateManager.updateCheckStatus
|
||||||
|
|
||||||
fun performUpdate() {
|
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?.cancel()
|
||||||
updateApplierJob = scope.launch {
|
updateApplierJob = scope.launch {
|
||||||
updateApplier.applyUpdate()
|
try {
|
||||||
|
updateManager.update()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showMessage(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showNewUpdate(versionData: VersionData) {
|
private fun showMessage(e: Exception) {
|
||||||
newVersionData.update { versionData }
|
e.printStackTrace()
|
||||||
|
notificationSender.sendDialogNotification(
|
||||||
|
"Update Error".asStringSource(),
|
||||||
|
e.localizedMessage.orEmpty().asStringSource(),
|
||||||
|
type = MessageDialogType.Error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showNewUpdate() {
|
||||||
showNewUpdate.update { true }
|
showNewUpdate.update { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestCheckForUpdate() {
|
fun requestCheckForUpdate() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
updateManager
|
||||||
updateCheckStatus = UpdateStatus.Checking
|
.checkForUpdate()
|
||||||
val result = updateChecker.check()
|
?.let {
|
||||||
if (result != null) {
|
showNewUpdate()
|
||||||
showNewUpdate(result)
|
|
||||||
updateCheckStatus = UpdateStatus.NewUpdate
|
|
||||||
} else {
|
|
||||||
updateCheckStatus = UpdateStatus.NoUpdate
|
|
||||||
}
|
}
|
||||||
updateCheckStatus = UpdateStatus.IDLE
|
|
||||||
}catch (e:Exception){
|
|
||||||
updateCheckStatus = UpdateStatus.Error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,38 +66,3 @@ class UpdateComponent(
|
|||||||
showNewUpdate.update { false }
|
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
|
package com.abdownloadmanager.desktop.pages.updater
|
||||||
|
|
||||||
import com.abdownloadmanager.desktop.ui.customwindow.CustomWindow
|
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.NotificationType
|
||||||
import com.abdownloadmanager.desktop.ui.widget.ShowNotification
|
import com.abdownloadmanager.desktop.ui.widget.ShowNotification
|
||||||
import com.abdownloadmanager.desktop.ui.widget.useNotification
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
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.compose.asStringSource
|
||||||
|
import ir.amirab.util.desktop.screen.applyUiScale
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -21,9 +26,9 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
|||||||
val closeUpdatePage = {
|
val closeUpdatePage = {
|
||||||
updaterComponent.requestClose()
|
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?) }
|
var notificationType by remember { mutableStateOf(null as NotificationType?) }
|
||||||
LaunchedEffect(status) {
|
LaunchedEffect(status) {
|
||||||
fun CoroutineScope.clearMessageAfter(delay: Long) {
|
fun CoroutineScope.clearMessageAfter(delay: Long) {
|
||||||
@ -33,23 +38,27 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (status) {
|
when (status) {
|
||||||
UpdateStatus.Checking -> {
|
UpdateCheckStatus.Checking -> {
|
||||||
message = "Checking for update"
|
message = Res.string.update_checking_for_update.asStringSource()
|
||||||
notificationType = NotificationType.Loading(null)
|
notificationType = NotificationType.Loading(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
is UpdateStatus.Error -> {
|
is UpdateCheckStatus.Error -> {
|
||||||
clearMessageAfter(3000)
|
clearMessageAfter(3000)
|
||||||
message = """
|
message = StringSource.CombinedStringSource(
|
||||||
Error while checking for update
|
listOf(
|
||||||
${status.e.localizedMessage}
|
Res.string.update_check_error.asStringSource(),
|
||||||
""".trimIndent()
|
status.e.localizedMessage.orEmpty().asStringSource(),
|
||||||
|
),
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
status.e.printStackTrace()
|
||||||
notificationType = NotificationType.Error
|
notificationType = NotificationType.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateStatus.NoUpdate -> {
|
UpdateCheckStatus.NoUpdate -> {
|
||||||
clearMessageAfter(3000)
|
clearMessageAfter(3000)
|
||||||
message = "No update"
|
message = Res.string.update_no_update.asStringSource()
|
||||||
notificationType = NotificationType.Info
|
notificationType = NotificationType.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,21 +71,23 @@ fun ShowUpdaterDialog(updaterComponent: UpdateComponent) {
|
|||||||
|
|
||||||
message?.let { message ->
|
message?.let { message ->
|
||||||
ShowNotification(
|
ShowNotification(
|
||||||
title = "Updater".asStringSource(),
|
title = Res.string.update_updater.asStringSource(),
|
||||||
description = message.asStringSource(),
|
description = message,
|
||||||
type = notificationType ?: NotificationType.Info,
|
type = notificationType ?: NotificationType.Info,
|
||||||
tag = "Updater"
|
tag = "Updater"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showUpdate && newVersion != null) {
|
if (showUpdate && newVersion != null) {
|
||||||
|
val uiScale = LocalUiScale.current
|
||||||
CustomWindow(
|
CustomWindow(
|
||||||
state = rememberWindowState(
|
state = rememberWindowState(
|
||||||
size = DpSize(400.dp, 400.dp)
|
size = DpSize(500.dp, 400.dp).applyUiScale(uiScale),
|
||||||
|
position = WindowPosition.Aligned(Alignment.Center)
|
||||||
),
|
),
|
||||||
onCloseRequest = closeUpdatePage,
|
onCloseRequest = closeUpdatePage,
|
||||||
) {
|
) {
|
||||||
NewUpdatePage(
|
NewUpdatePage(
|
||||||
versionVersionData = newVersion,
|
newVersionInfo = newVersion,
|
||||||
currentVersion = updaterComponent.currentVersion,
|
currentVersion = updaterComponent.currentVersion,
|
||||||
cancel = closeUpdatePage,
|
cancel = closeUpdatePage,
|
||||||
update = {
|
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.editdownload.EditDownloadWindow
|
||||||
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
import com.abdownloadmanager.desktop.pages.home.HomeWindow
|
||||||
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
import com.abdownloadmanager.desktop.pages.settings.ThemeManager
|
||||||
|
import com.abdownloadmanager.desktop.pages.updater.ShowUpdaterDialog
|
||||||
import com.abdownloadmanager.desktop.ui.widget.*
|
import com.abdownloadmanager.desktop.ui.widget.*
|
||||||
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
import com.abdownloadmanager.utils.compose.ProvideDebugInfo
|
||||||
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
import ir.amirab.util.compose.localizationmanager.LanguageManager
|
||||||
@ -89,8 +90,7 @@ object Ui : KoinComponent {
|
|||||||
ShowAddDownloadDialogs(appComponent)
|
ShowAddDownloadDialogs(appComponent)
|
||||||
ShowDownloadDialogs(appComponent)
|
ShowDownloadDialogs(appComponent)
|
||||||
ShowCategoryDialogs(appComponent)
|
ShowCategoryDialogs(appComponent)
|
||||||
//TODO Enable Updater
|
ShowUpdaterDialog(appComponent.updater)
|
||||||
//ShowUpdaterDialog(appComponent.updater)
|
|
||||||
ShowAboutDialog(appComponent)
|
ShowAboutDialog(appComponent)
|
||||||
NewQueueDialog(appComponent)
|
NewQueueDialog(appComponent)
|
||||||
ShowMessageDialogs(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 {
|
object AppInfo {
|
||||||
val name = SharedConstants.appName
|
val name = SharedConstants.appName
|
||||||
|
val displayName = SharedConstants.appDisplayName
|
||||||
val packageName = SharedConstants.packageName
|
val packageName = SharedConstants.packageName
|
||||||
val website = SharedConstants.projectWebsite
|
val website = SharedConstants.projectWebsite
|
||||||
val sourceCode = SharedConstants.projectSourceCode
|
val sourceCode = SharedConstants.projectSourceCode
|
||||||
@ -21,6 +22,18 @@ object AppInfo {
|
|||||||
// }
|
// }
|
||||||
System.getProperty("jpackage.app-path")
|
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 {
|
fun AppInfo.isAppInstalled(): Boolean {
|
||||||
@ -36,8 +49,11 @@ fun AppInfo.isInDebugMode(): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val AppInfo.configDir: File get() = File(AppProperties.getConfigDirectory())
|
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.optionsDir: File get() = AppInfo.configDir.resolve("options")
|
||||||
val AppInfo.downloadDbDir:File get() = AppInfo.configDir.resolve("download_db")
|
val AppInfo.downloadDbDir: File get() = AppInfo.configDir.resolve("download_db")
|
||||||
fun AppInfo.extensions(){
|
fun AppInfo.extensions() {
|
||||||
|
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ object AppProperties {
|
|||||||
|
|
||||||
private object Keys {
|
private object Keys {
|
||||||
const val CONFIG_DIRECTORY: String = "app.config.path"
|
const val CONFIG_DIRECTORY: String = "app.config.path"
|
||||||
|
const val SYSTEM_DIRECTORY: String = "app.system.path"
|
||||||
const val DEBUG: String = "app.debug"
|
const val DEBUG: String = "app.debug"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +82,11 @@ object AppProperties {
|
|||||||
return ensureAndGet(Keys.CONFIG_DIRECTORY)
|
return ensureAndGet(Keys.CONFIG_DIRECTORY)
|
||||||
.toString()
|
.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSystemDirectory(): String {
|
||||||
|
return ensureAndGet(Keys.SYSTEM_DIRECTORY)
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
//app.properties in installation directory
|
//app.properties in installation directory
|
||||||
fun isAppPropertiesFound(): Boolean {
|
fun isAppPropertiesFound(): Boolean {
|
||||||
return foundAppProperties
|
return foundAppProperties
|
||||||
|
@ -4,8 +4,6 @@ import com.abdownloadmanager.desktop.utils.AppInfo
|
|||||||
import com.abdownloadmanager.desktop.utils.isAppInstalled
|
import com.abdownloadmanager.desktop.utils.isAppInstalled
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
data class NativeMessagingManifests(
|
data class NativeMessagingManifests(
|
||||||
val firefoxNativeMessagingManifest: FirefoxNativeMessagingManifest,
|
val firefoxNativeMessagingManifest: FirefoxNativeMessagingManifest,
|
||||||
@ -65,8 +63,8 @@ class NativeMessaging(
|
|||||||
if (!AppInfo.isAppInstalled()) return null
|
if (!AppInfo.isAppInstalled()) return null
|
||||||
val execFile = AppInfo.exeFile!!
|
val execFile = AppInfo.exeFile!!
|
||||||
return FirefoxNativeMessagingManifest(
|
return FirefoxNativeMessagingManifest(
|
||||||
name = AppInfo.name,
|
name = AppInfo.displayName,
|
||||||
description = AppInfo.name,
|
description = AppInfo.displayName,
|
||||||
path = execFile,
|
path = execFile,
|
||||||
type = "stdio",
|
type = "stdio",
|
||||||
allowedExtensions = listOf(
|
allowedExtensions = listOf(
|
||||||
@ -78,8 +76,8 @@ class NativeMessaging(
|
|||||||
if (!AppInfo.isAppInstalled()) return null
|
if (!AppInfo.isAppInstalled()) return null
|
||||||
val execFile = AppInfo.exeFile!!
|
val execFile = AppInfo.exeFile!!
|
||||||
return ChromeNativeMessagingManifest(
|
return ChromeNativeMessagingManifest(
|
||||||
name = AppInfo.name,
|
name = AppInfo.displayName,
|
||||||
description = AppInfo.name,
|
description = AppInfo.displayName,
|
||||||
path = execFile,
|
path = execFile,
|
||||||
type = "stdio",
|
type = "stdio",
|
||||||
allowedOrigins = listOf(
|
allowedOrigins = listOf(
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
app.config.path="${user.home}/.abdm/config"
|
app.config.path="${user.home}/.abdm/config"
|
||||||
|
app.system.path="${user.home}/.abdm/system"
|
||||||
app.debug="false"
|
app.debug="false"
|
||||||
|
@ -260,8 +260,8 @@ class DownloadMonitor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun waitForDownloadToFinishOrCancel(
|
override suspend fun waitForDownloadToFinishOrCancel(
|
||||||
id: Long
|
id: Long,
|
||||||
): Boolean {
|
) {
|
||||||
val event = downloadManager
|
val event = downloadManager
|
||||||
.listOfJobsEvents
|
.listOfJobsEvents
|
||||||
.filter {
|
.filter {
|
||||||
@ -278,10 +278,8 @@ class DownloadMonitor(
|
|||||||
is DownloadManagerEvents.OnJobStarting -> false
|
is DownloadManagerEvents.OnJobStarting -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event is DownloadManagerEvents.OnJobCompleted) {
|
if (event is DownloadManagerEvents.OnJobCanceled) {
|
||||||
return true
|
throw event.e
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,8 +14,8 @@ interface IDownloadMonitor {
|
|||||||
val activeDownloadCount: StateFlow<Int>
|
val activeDownloadCount: StateFlow<Int>
|
||||||
|
|
||||||
suspend fun waitForDownloadToFinishOrCancel(
|
suspend fun waitForDownloadToFinishOrCancel(
|
||||||
id: Long
|
id: Long,
|
||||||
): Boolean
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun IDownloadMonitor.isDownloadActiveFlow(
|
fun IDownloadMonitor.isDownloadActiveFlow(
|
||||||
|
@ -26,6 +26,7 @@ semver = "2.0.0"
|
|||||||
jgit = "6.9.0.202403050737-r"
|
jgit = "6.9.0.202403050737-r"
|
||||||
osThemeDetector = "3.9.1"
|
osThemeDetector = "3.9.1"
|
||||||
kotlinFileWatcher = "1.3.0"
|
kotlinFileWatcher = "1.3.0"
|
||||||
|
markdownRenderer = "0.27.0"
|
||||||
|
|
||||||
[libraries]
|
[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" }
|
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||||
osThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "osThemeDetector" }
|
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"
|
handlebarsJava = "com.github.jknack:handlebars:4.4.0"
|
||||||
|
|
||||||
kotlinFileWatcher = { module = "io.github.irgaly.kfswatch:kfswatch", version.ref = "kotlinFileWatcher" }
|
kotlinFileWatcher = { module = "io.github.irgaly.kfswatch:kfswatch", version.ref = "kotlinFileWatcher" }
|
||||||
|
@ -126,10 +126,12 @@ class DownloadSystem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun manualResume(id: Long): Boolean {
|
suspend fun manualResume(id: Long): Boolean {
|
||||||
// if (mainDownloadQueue.isQueueActive) {
|
manualResume(id, ResumedBy(User))
|
||||||
// return false
|
return true
|
||||||
// }
|
}
|
||||||
downloadManager.resume(id, ResumedBy(User))
|
|
||||||
|
suspend fun manualResume(id: Long, context: DownloadItemContext): Boolean {
|
||||||
|
downloadManager.resume(id, context)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,6 +213,11 @@ class DownloadSystem(
|
|||||||
it.getFullPath().path == path
|
it.getFullPath().path == path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun getDownloadItemsByFolder(folder: String): List<IDownloadItemState> {
|
||||||
|
return downloadMonitor.downloadListFlow.value.filter {
|
||||||
|
it.folder == folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun getFilePathById(id: Long): File? {
|
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
|
||||||
|
}
|
@ -315,4 +315,14 @@ contribute=Contribute
|
|||||||
meet_the_translators=Meet the Translators
|
meet_the_translators=Meet the Translators
|
||||||
localized_by_translators=Localized by Translators
|
localized_by_translators=Localized by Translators
|
||||||
confirm_exit=Confirm Exit
|
confirm_exit=Confirm Exit
|
||||||
confirm_exit_description=Are you sure you want to exit AB Download Manager?\nActive downloads/queues will be stopped!
|
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.okhttp.okhttp)
|
||||||
api(libs.kotlin.coroutines.core)
|
api(libs.kotlin.coroutines.core)
|
||||||
implementation(project(":shared:utils"))
|
implementation(project(":shared:utils"))
|
||||||
|
implementation(libs.jna.platform)
|
||||||
|
implementation(libs.semver)
|
||||||
}
|
}
|
@ -1,44 +1,37 @@
|
|||||||
package com.abdownloadmanager
|
package com.abdownloadmanager
|
||||||
|
|
||||||
import io.github.z4kn4fein.semver.Version
|
import io.github.z4kn4fein.semver.Version
|
||||||
|
import ir.amirab.util.platform.Arch
|
||||||
import ir.amirab.util.platform.Platform
|
import ir.amirab.util.platform.Platform
|
||||||
|
|
||||||
data class AppArtifactInfo(
|
data class AppArtifactInfo(
|
||||||
val version: Version,
|
val version: Version,
|
||||||
val platform: Platform,
|
val platform: Platform,
|
||||||
|
val arch: Arch,
|
||||||
)
|
)
|
||||||
|
|
||||||
object ArtifactUtil {
|
object ArtifactUtil {
|
||||||
private val versionPatern = "(\\d+\\.\\d+\\.\\d+)"
|
val artifactRegex =
|
||||||
val versionRegex = "_$versionPatern".toRegex()
|
"(?<appName>[a-zA-Z]+)_(?<version>(\\d+\\.\\d+\\.\\d+))_(?<platform>[a-zA-Z]+)_(?<arch>[a-zA-Z0-9]+)\\.(?<extension>.+)".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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun getArtifactInfo(name: String): AppArtifactInfo? {
|
fun getArtifactInfo(name: String): AppArtifactInfo? {
|
||||||
val version = extractVersion(name) ?: return null
|
val values = artifactRegex.find(name)?.groups ?: return null
|
||||||
val platform = extractPlatformFromName(name) ?: 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(
|
return AppArtifactInfo(
|
||||||
version = version,
|
version = version,
|
||||||
platform = platform,
|
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
|
package com.abdownloadmanager.updateapplier
|
||||||
|
|
||||||
abstract class UpdateApplier{
|
import com.abdownloadmanager.updatechecker.UpdateInfo
|
||||||
abstract suspend fun applyUpdate()
|
|
||||||
|
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,66 +1,17 @@
|
|||||||
package com.abdownloadmanager.updatechecker
|
package com.abdownloadmanager.updatechecker
|
||||||
|
|
||||||
import GithubApi
|
|
||||||
import com.abdownloadmanager.ArtifactUtil
|
|
||||||
import io.github.z4kn4fein.semver.Version
|
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(
|
abstract class UpdateChecker(
|
||||||
protected val currentVersion: Version,
|
protected val currentVersion: Version,
|
||||||
) {
|
) {
|
||||||
abstract suspend fun getMyPlatformLatestVersion(): VersionData
|
abstract suspend fun getMyPlatformLatestVersion(): UpdateInfo
|
||||||
suspend fun check(): VersionData? {
|
suspend fun check(): UpdateInfo? {
|
||||||
val latest=getMyPlatformLatestVersion()
|
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 {
|
return latest.takeIf {
|
||||||
it.version>currentVersion
|
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