Merge pull request #70 from amir1376/feat/improve-download-page

feat/improve download page
This commit is contained in:
AmirHossein Abdolmotallebi 2024-09-12 07:45:46 +03:30 committed by GitHub
commit f10cc43ce0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 163 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

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