mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
Merge pull request #70 from amir1376/feat/improve-download-page
feat/improve download page
This commit is contained in:
commit
f10cc43ce0
@ -43,10 +43,11 @@ fun getDownloadTitle(itemState: IDownloadItemState): String {
|
||||
}
|
||||
}
|
||||
|
||||
val LocalSingleBoxSizing = compositionLocalOf<SingleDownloadPageSizing> { error("LocalSingleBoxSizing not provided") }
|
||||
val LocalSingleDownloadPageSizing = compositionLocalOf<SingleDownloadPageSizing> { error("LocalSingleBoxSizing not provided") }
|
||||
|
||||
@Stable
|
||||
class SingleDownloadPageSizing {
|
||||
var resizingPartInfo by mutableStateOf(false)
|
||||
var partInfoHeight by mutableStateOf(150.dp)
|
||||
}
|
||||
|
||||
@ -60,7 +61,7 @@ fun ShowDownloadDialogs(component: DownloadDialogManager) {
|
||||
val defaultHeight = 290f
|
||||
val defaultWidth = 450f
|
||||
|
||||
val showPartInfo by singleDownloadComponent.showPartInfo
|
||||
val showPartInfo by singleDownloadComponent.showPartInfo.collectAsState()
|
||||
val itemState by singleDownloadComponent.itemStateFlow.collectAsState()
|
||||
val state = rememberWindowState(
|
||||
height = defaultHeight.dp,
|
||||
@ -92,15 +93,17 @@ fun ShowDownloadDialogs(component: DownloadDialogManager) {
|
||||
if (showPartInfo && itemState is ProcessingDownloadItemState) {
|
||||
h += singleDownloadPageSizing.partInfoHeight.value
|
||||
}
|
||||
state.size = DpSize(
|
||||
width = w.dp,
|
||||
height = h.dp
|
||||
)
|
||||
LaunchedEffect(w, h) {
|
||||
state.size = DpSize(
|
||||
width = w.dp,
|
||||
height = h.dp
|
||||
)
|
||||
}
|
||||
itemState?.let { itemState ->
|
||||
UpdateTaskBar(window, itemState)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalSingleBoxSizing provides singleDownloadPageSizing
|
||||
LocalSingleDownloadPageSizing provides singleDownloadPageSizing
|
||||
) {
|
||||
SingleDownloadPage(singleDownloadComponent)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@ -56,7 +57,8 @@ private val tabs = SingleDownloadPageSections.entries.toList()
|
||||
fun SingleDownloadPage(singleDownloadComponent: SingleDownloadComponent) {
|
||||
val itemState = singleDownloadComponent.itemStateFlow.collectAsState().value
|
||||
var selectedTab by remember { mutableStateOf(Info) }
|
||||
val (showPartInfo, setShowPartInfo) = singleDownloadComponent.showPartInfo
|
||||
val showPartInfo by singleDownloadComponent.showPartInfo.collectAsState()
|
||||
val setShowPartInfo = singleDownloadComponent::setShowPartInfo
|
||||
if (itemState != null) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 16.dp)
|
||||
@ -116,6 +118,12 @@ fun SingleDownloadPage(singleDownloadComponent: SingleDownloadComponent) {
|
||||
RenderActions(itemState, singleDownloadComponent, showPartInfo, setShowPartInfo)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
}
|
||||
val resizingState = LocalSingleDownloadPageSizing.current
|
||||
LaunchedEffect(resizingState.resizingPartInfo){
|
||||
if (resizingState.partInfoHeight<=0.dp){
|
||||
setShowPartInfo(false)
|
||||
}
|
||||
}
|
||||
if (showPartInfo && itemState is ProcessingDownloadItemState) {
|
||||
RenderPartInfo(itemState)
|
||||
}
|
||||
@ -160,7 +168,7 @@ fun RenderProgressBar(itemState: IDownloadItemState) {
|
||||
is DownloadJobStatus.PreparingFile -> myColors.infoGradient
|
||||
DownloadJobStatus.Resuming,
|
||||
DownloadJobStatus.Downloading,
|
||||
-> myColors.primaryGradient
|
||||
-> myColors.primaryGradient
|
||||
}
|
||||
|
||||
Box(
|
||||
@ -182,7 +190,7 @@ fun RenderProgressBar(itemState: IDownloadItemState) {
|
||||
).value
|
||||
)
|
||||
) {
|
||||
if(progress==1f){
|
||||
if (progress == 1f) {
|
||||
MyIcon(
|
||||
MyIcons.check,
|
||||
null,
|
||||
@ -206,26 +214,28 @@ fun RenderProgressBar(itemState: IDownloadItemState) {
|
||||
1f,
|
||||
infiniteRepeatable(tween(l), RepeatMode.Restart)
|
||||
)
|
||||
val width by anim.animateFloat(6f, 16f, infiniteRepeatable(
|
||||
keyframes {
|
||||
durationMillis = l
|
||||
0f atFraction 0f
|
||||
0.75f atFraction 0.25f
|
||||
0f atFraction 1f
|
||||
},
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
val width by anim.animateFloat(
|
||||
6f, 16f, infiniteRepeatable(
|
||||
keyframes {
|
||||
durationMillis = l
|
||||
0f atFraction 0f
|
||||
0.75f atFraction 0.25f
|
||||
0f atFraction 1f
|
||||
},
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(endPos)
|
||||
) {
|
||||
Box(Modifier
|
||||
.background(background)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxWidth(width)
|
||||
Box(
|
||||
Modifier
|
||||
.background(background)
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxWidth(width)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -256,7 +266,7 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
||||
.let { parts ->
|
||||
if (onlyActiveParts) {
|
||||
parts.filter {
|
||||
when(it.status){
|
||||
when (it.status) {
|
||||
is PartDownloadStatus.Canceled -> true
|
||||
PartDownloadStatus.Completed -> false
|
||||
PartDownloadStatus.IDLE -> false
|
||||
@ -331,7 +341,8 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
||||
SimpleCellText(
|
||||
"${
|
||||
it.value.length?.let { length ->
|
||||
convertSizeToHumanReadable(length
|
||||
convertSizeToHumanReadable(
|
||||
length
|
||||
)
|
||||
} ?: "Unknown"
|
||||
}",
|
||||
@ -354,15 +365,24 @@ fun ColumnScope.RenderPartInfo(itemState: ProcessingDownloadItemState) {
|
||||
}
|
||||
}
|
||||
}
|
||||
val singleDownloadPageSizing = LocalSingleBoxSizing.current
|
||||
Handle(Modifier.fillMaxWidth().height(8.dp), orientation = Orientation.Vertical) {
|
||||
val singleDownloadPageSizing = LocalSingleDownloadPageSizing.current
|
||||
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||
val isDraggingHandle by mutableInteractionSource.collectIsDraggedAsState()
|
||||
LaunchedEffect(isDraggingHandle){
|
||||
singleDownloadPageSizing.resizingPartInfo = isDraggingHandle
|
||||
}
|
||||
Handle(
|
||||
Modifier.fillMaxWidth().height(8.dp),
|
||||
orientation = Orientation.Vertical,
|
||||
interactionSource = mutableInteractionSource
|
||||
) {
|
||||
singleDownloadPageSizing.partInfoHeight += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prettifyStatus(status: PartDownloadStatus): String {
|
||||
return when(status){
|
||||
return when (status) {
|
||||
is PartDownloadStatus.Canceled -> "Disconnected"
|
||||
PartDownloadStatus.IDLE -> "IDLE"
|
||||
PartDownloadStatus.Completed -> "Completed"
|
||||
@ -412,8 +432,8 @@ sealed class PartInfoCells : TableCell<IndexedValue<UiPart>> {
|
||||
|
||||
@Composable
|
||||
fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
|
||||
val title= propertyItem.name
|
||||
val value= propertyItem.value
|
||||
val title = propertyItem.name
|
||||
val value = propertyItem.value
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@ -434,7 +454,7 @@ fun RenderPropertyItem(propertyItem: SingleDownloadPagePropertyItem) {
|
||||
.weight(0.7f),
|
||||
maxLines = 1,
|
||||
fontSize = myTextSizes.base,
|
||||
color = when(propertyItem.valueState){
|
||||
color = when (propertyItem.valueState) {
|
||||
SingleDownloadPagePropertyItem.ValueType.Normal -> LocalContentColor.current
|
||||
SingleDownloadPagePropertyItem.ValueType.Error -> myColors.error
|
||||
SingleDownloadPagePropertyItem.ValueType.Success -> myColors.success
|
||||
@ -489,7 +509,8 @@ fun RenderActions(
|
||||
is ProcessingDownloadItemState -> {
|
||||
PartInfoButton(showingPartInfo, onRequestShowPartInfo)
|
||||
Spacer(Modifier.weight(1f))
|
||||
ToggleButton(it,
|
||||
ToggleButton(
|
||||
it,
|
||||
singleDownloadComponent::toggle,
|
||||
singleDownloadComponent::resume,
|
||||
singleDownloadComponent::pause,
|
||||
@ -597,29 +618,29 @@ private fun ToggleButton(
|
||||
Box {
|
||||
SingleDownloadPageButton(
|
||||
{
|
||||
if (isResumeSupported){
|
||||
if (isResumeSupported) {
|
||||
toggle()
|
||||
}else{
|
||||
if (itemState.status is DownloadJobStatus.IsActive){
|
||||
showPromptOnNonePresumablePause=true
|
||||
}else{
|
||||
} else {
|
||||
if (itemState.status is DownloadJobStatus.IsActive) {
|
||||
showPromptOnNonePresumablePause = true
|
||||
} else {
|
||||
toggle()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = icon,
|
||||
text = text,
|
||||
color = if (isResumeSupported){
|
||||
color = if (isResumeSupported) {
|
||||
LocalContentColor.current
|
||||
}else{
|
||||
if (itemState.status is DownloadJobStatus.IsActive){
|
||||
} else {
|
||||
if (itemState.status is DownloadJobStatus.IsActive) {
|
||||
myColors.error
|
||||
}else{
|
||||
} else {
|
||||
LocalContentColor.current
|
||||
}
|
||||
},
|
||||
)
|
||||
if (showPromptOnNonePresumablePause){
|
||||
if (showPromptOnNonePresumablePause) {
|
||||
val shape = RoundedCornerShape(6.dp)
|
||||
val closePopup = {
|
||||
showPromptOnNonePresumablePause = false
|
||||
@ -648,7 +669,7 @@ private fun ToggleButton(
|
||||
.widthIn(max = 140.dp)
|
||||
) {
|
||||
Text(buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = myColors.warning)){
|
||||
withStyle(SpanStyle(color = myColors.warning)) {
|
||||
append("WARNING:\n")
|
||||
}
|
||||
append("This download doesn't support resuming! You may have to RESTART it later in the Download List")
|
||||
|
@ -6,7 +6,8 @@ import com.abdownloadmanager.desktop.pages.settings.configurable.SpeedLimitConfi
|
||||
import com.abdownloadmanager.desktop.utils.*
|
||||
import com.abdownloadmanager.desktop.utils.mvi.ContainsEffects
|
||||
import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import arrow.optics.copy
|
||||
import com.abdownloadmanager.desktop.storage.PageStatesStorage
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.DownloadManagerEvents
|
||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||
@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
|
||||
sealed interface SingleDownloadEffects {
|
||||
@ -31,6 +33,7 @@ data class SingleDownloadPagePropertyItem(
|
||||
) {
|
||||
enum class ValueType { Normal, Error, Success }
|
||||
}
|
||||
|
||||
class SingleDownloadComponent(
|
||||
ctx: ComponentContext,
|
||||
val downloadItemOpener: DownloadItemOpener,
|
||||
@ -40,14 +43,25 @@ class SingleDownloadComponent(
|
||||
ContainsEffects<SingleDownloadEffects> by supportEffects(),
|
||||
KoinComponent {
|
||||
private val downloadSystem: DownloadSystem by inject()
|
||||
private val singleDownloadPageStateToPersist by lazy {
|
||||
get<PageStatesStorage>().downloadPage
|
||||
}
|
||||
private val downloadMonitor: IDownloadMonitor = downloadSystem.downloadMonitor
|
||||
private val downloadManager: DownloadManager = downloadSystem.downloadManager
|
||||
val itemStateFlow = downloadMonitor.downloadListFlow.map {
|
||||
it.firstOrNull { it.id == downloadId }
|
||||
}.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
|
||||
val showPartInfo = mutableStateOf(false)
|
||||
|
||||
private val _showPartInfo = MutableStateFlow(singleDownloadPageStateToPersist.value.showPartInfo)
|
||||
val showPartInfo = _showPartInfo.asStateFlow()
|
||||
fun setShowPartInfo(value: Boolean) {
|
||||
_showPartInfo.value = value
|
||||
singleDownloadPageStateToPersist.update {
|
||||
it.copy {
|
||||
SingleDownloadPageStateToPersist.showPartInfo.set(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val extraDownloadInfo: StateFlow<List<SingleDownloadPagePropertyItem>> = itemStateFlow
|
||||
.filterNotNull()
|
||||
@ -61,24 +75,31 @@ class SingleDownloadComponent(
|
||||
}
|
||||
|
||||
is ProcessingDownloadItemState -> {
|
||||
add(SingleDownloadPagePropertyItem("Downloaded" , convertBytesToHumanReadable(it.progress).orEmpty()))
|
||||
add(SingleDownloadPagePropertyItem("Speed" , convertSpeedToHumanReadable(it.speed)))
|
||||
add(SingleDownloadPagePropertyItem("Remaining Time" , (it.remainingTime?.let { remainingTime ->
|
||||
add(
|
||||
SingleDownloadPagePropertyItem(
|
||||
"Downloaded",
|
||||
convertBytesToHumanReadable(it.progress).orEmpty()
|
||||
)
|
||||
)
|
||||
add(SingleDownloadPagePropertyItem("Speed", convertSpeedToHumanReadable(it.speed)))
|
||||
add(SingleDownloadPagePropertyItem("Remaining Time", (it.remainingTime?.let { remainingTime ->
|
||||
convertTimeRemainingToHumanReadable(remainingTime, TimeNames.ShortNames)
|
||||
}.orEmpty())))
|
||||
add(SingleDownloadPagePropertyItem(
|
||||
"Resume Support",
|
||||
when (it.supportResume) {
|
||||
true -> "Yes"
|
||||
false -> "No"
|
||||
null -> "Unknown"
|
||||
},
|
||||
when (it.supportResume) {
|
||||
true -> SingleDownloadPagePropertyItem.ValueType.Success
|
||||
false -> SingleDownloadPagePropertyItem.ValueType.Error
|
||||
null -> SingleDownloadPagePropertyItem.ValueType.Normal
|
||||
}
|
||||
))
|
||||
add(
|
||||
SingleDownloadPagePropertyItem(
|
||||
"Resume Support",
|
||||
when (it.supportResume) {
|
||||
true -> "Yes"
|
||||
false -> "No"
|
||||
null -> "Unknown"
|
||||
},
|
||||
when (it.supportResume) {
|
||||
true -> SingleDownloadPagePropertyItem.ValueType.Success
|
||||
false -> SingleDownloadPagePropertyItem.ValueType.Error
|
||||
null -> SingleDownloadPagePropertyItem.ValueType.Normal
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,6 +156,7 @@ class SingleDownloadComponent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
val state = itemStateFlow.value as ProcessingDownloadItemState ?: return
|
||||
scope.launch {
|
||||
@ -143,6 +165,7 @@ class SingleDownloadComponent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
val state = itemStateFlow.value as ProcessingDownloadItemState ?: return
|
||||
scope.launch {
|
||||
@ -170,14 +193,14 @@ class SingleDownloadComponent(
|
||||
downloadManager.dlListDb.getById(downloadId)
|
||||
}
|
||||
threadCount = MutableStateFlow(
|
||||
dItem?.preferredConnectionCount ?:0
|
||||
dItem?.preferredConnectionCount ?: 0
|
||||
)
|
||||
speedLimit = MutableStateFlow(dItem?.speedLimit ?: 0)
|
||||
downloadManager.listOfJobsEvents
|
||||
.filterIsInstance<DownloadManagerEvents.OnJobChanged>()
|
||||
.onEach { event ->
|
||||
threadCount.update {
|
||||
event.downloadItem.preferredConnectionCount?:0
|
||||
event.downloadItem.preferredConnectionCount ?: 0
|
||||
}
|
||||
speedLimit.update {
|
||||
event.downloadItem.speedLimit
|
||||
@ -190,7 +213,7 @@ class SingleDownloadComponent(
|
||||
.debounce(500)
|
||||
.onEach { count ->
|
||||
downloadManager.updateDownloadItem(downloadId) {
|
||||
it.preferredConnectionCount = count.takeIf { it>0 }
|
||||
it.preferredConnectionCount = count.takeIf { it > 0 }
|
||||
}
|
||||
}.launchIn(scope)
|
||||
speedLimit
|
||||
@ -211,9 +234,9 @@ class SingleDownloadComponent(
|
||||
description = "How much thread used to download this item 0 for default",
|
||||
backedBy = threadCount,
|
||||
describe = {
|
||||
if (it==0){
|
||||
if (it == 0) {
|
||||
"uses global setting"
|
||||
}else{
|
||||
} else {
|
||||
"$it threads"
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,36 @@
|
||||
package com.abdownloadmanager.desktop.pages.singleDownloadPage
|
||||
|
||||
import arrow.optics.Lens
|
||||
import arrow.optics.optics
|
||||
import ir.amirab.util.config.MapConfig
|
||||
import ir.amirab.util.config.booleanKeyOf
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@optics
|
||||
@Serializable
|
||||
data class SingleDownloadPageStateToPersist(
|
||||
val showPartInfo: Boolean = false,
|
||||
) {
|
||||
class ConfigLens(prefix: String) : Lens<MapConfig, SingleDownloadPageStateToPersist>,
|
||||
KoinComponent {
|
||||
class Keys(prefix: String) {
|
||||
val showPartInfo = booleanKeyOf("${prefix}showPartInfo")
|
||||
}
|
||||
|
||||
private val keys = Keys(prefix)
|
||||
override fun get(source: MapConfig): SingleDownloadPageStateToPersist {
|
||||
val default by lazy { SingleDownloadPageStateToPersist() }
|
||||
return SingleDownloadPageStateToPersist(
|
||||
showPartInfo = source.get(keys.showPartInfo) ?: default.showPartInfo,
|
||||
)
|
||||
}
|
||||
|
||||
override fun set(source: MapConfig, focus: SingleDownloadPageStateToPersist): MapConfig {
|
||||
source.put(keys.showPartInfo, focus.showPartInfo)
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
@ -5,6 +5,7 @@ import com.abdownloadmanager.desktop.utils.*
|
||||
import androidx.datastore.core.DataStore
|
||||
import arrow.optics.Lens
|
||||
import arrow.optics.optics
|
||||
import com.abdownloadmanager.desktop.pages.singleDownloadPage.SingleDownloadPageStateToPersist
|
||||
import ir.amirab.util.config.getDecoded
|
||||
import ir.amirab.util.config.keyOfEncoded
|
||||
import ir.amirab.util.config.putEncoded
|
||||
@ -48,6 +49,7 @@ data class CommonData(
|
||||
@Serializable
|
||||
data class PageStatesModel(
|
||||
val home: HomePageStateToPersist = HomePageStateToPersist(),
|
||||
val downloadPage: SingleDownloadPageStateToPersist = SingleDownloadPageStateToPersist(),
|
||||
val global: CommonData = CommonData(),
|
||||
) {
|
||||
companion object {
|
||||
@ -59,23 +61,22 @@ data class PageStatesModel(
|
||||
|
||||
object Child {
|
||||
val common = CommonData.ConfigLens("global.")
|
||||
val downloadPage = SingleDownloadPageStateToPersist.ConfigLens("downloadPage.")
|
||||
val home = HomePageStateToPersist.ConfigLens("home.")
|
||||
}
|
||||
|
||||
override fun get(source: MapConfig): PageStatesModel {
|
||||
return with(json) {
|
||||
PageStatesModel(
|
||||
home = Child.home.get(source),
|
||||
global = Child.common.get(source)
|
||||
)
|
||||
}
|
||||
return PageStatesModel(
|
||||
home = Child.home.get(source),
|
||||
downloadPage = Child.downloadPage.get(source),
|
||||
global = Child.common.get(source)
|
||||
)
|
||||
}
|
||||
|
||||
override fun set(source: MapConfig, focus: PageStatesModel): MapConfig {
|
||||
with(json) {
|
||||
Child.home.set(source, focus.home)
|
||||
Child.common.set(source, focus.global)
|
||||
}
|
||||
Child.home.set(source, focus.home)
|
||||
Child.downloadPage.set(source, focus.downloadPage)
|
||||
Child.common.set(source, focus.global)
|
||||
return source
|
||||
}
|
||||
}
|
||||
@ -85,6 +86,6 @@ class PageStatesStorage(
|
||||
settings: DataStore<MapConfig>,
|
||||
) : ConfigBaseSettings<PageStatesModel>(settings, PageStatesModel.ConfigLens) {
|
||||
val lastUsedSaveLocations = from(PageStatesModel.global.lastSavedLocations)
|
||||
|
||||
val downloadPage = from(PageStatesModel.downloadPage)
|
||||
val homePageStorage = from(PageStatesModel.home)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user