added in app update (#319)

This commit is contained in:
AmirHossein Abdolmotallebi 2024-12-26 08:43:19 +03:30 committed by GitHub
parent 8a24d5e06d
commit 4d5a38938c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1356 additions and 335 deletions

View File

@ -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 {

View File

@ -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"

View File

@ -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

View File

@ -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)
} }

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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,
)
}
} }

View File

@ -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,
) )

View File

@ -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(

View File

@ -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(),
) )

View File

@ -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,
)
} }
} }

View File

@ -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)
}
}

View File

@ -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
)
}
}
}

View File

@ -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 = {

View File

@ -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)

View File

@ -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,
),
)
}

View File

@ -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() {
} }

View File

@ -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

View File

@ -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(

View File

@ -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"

View File

@ -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
} }
} }
} }

View File

@ -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(

View File

@ -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" }

View File

@ -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? {

View 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()
}
}
}

View File

@ -0,0 +1,5 @@
package com.abdownloadmanager.utils.compose
fun androidx.compose.foundation.v2.ScrollbarAdapter.needScroll(): Boolean {
return contentSize > viewportSize
}

View File

@ -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}}

View File

@ -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)
} }

View File

@ -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,
) )
} }
} }

View File

@ -0,0 +1,7 @@
package com.abdownloadmanager
import java.io.File
fun interface UpdateDownloadLocationProvider {
fun getSaveLocation(): File
}

View 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
// ...
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()
} }

View File

@ -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()
}

View File

@ -0,0 +1,6 @@
package com.abdownloadmanager.updateapplier
interface UpdateInstaller {
fun installUpdate()
}

View File

@ -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()
}
}

View File

@ -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}")
}
}

View File

@ -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()
)
}
}

View File

@ -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
)
}
}

View File

@ -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
} }
} }
} }

View File

@ -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
}

View File

@ -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,
)

View File

@ -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 "$@"

View File

@ -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

View File

@ -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()
}