Skip to content

Commit cb0b64b

Browse files
authored
Merge pull request #604 from namehillsoftware/feature/downloading-item-as-top-item
[Feature] Downloading Item as Top Item
2 parents 8363d96 + bd15c0e commit cb0b64b

File tree

26 files changed

+482
-146
lines changed

26 files changed

+482
-146
lines changed

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/files/details/FileDetailsViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import com.lasthopesoftware.bluewater.shared.messages.registerReceiver
2222
import com.lasthopesoftware.observables.LiftedInteractionState
2323
import com.lasthopesoftware.observables.MutableInteractionState
2424
import com.lasthopesoftware.observables.mapNotNull
25-
import com.lasthopesoftware.observables.toMaybeObservable
25+
import com.lasthopesoftware.observables.toSingleObservable
2626
import com.lasthopesoftware.promises.extensions.keepPromise
2727
import com.lasthopesoftware.promises.extensions.preparePromise
2828
import com.lasthopesoftware.promises.extensions.unitResponse
@@ -82,7 +82,7 @@ class FileDetailsViewModel(
8282
override val isLoading = mutableIsLoading.asInteractionState()
8383
override val coverArt = LiftedInteractionState(
8484
promisedDefaultCoverArt
85-
.toMaybeObservable()
85+
.toSingleObservable()
8686
.toObservable()
8787
.concatWith(mutableCoverArt.mapNotNull()),
8888
emptyByteArray

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/navigation/NavigateToLibraryDestination.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.lasthopesoftware.bluewater.client.browsing.navigation
22

33
import LoadedItemListView
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
56
import androidx.compose.runtime.LaunchedEffect
67
import androidx.compose.runtime.getValue
78
import androidx.compose.runtime.mutableStateOf
@@ -39,7 +40,13 @@ fun ScreenDimensionsScope.NavigateToLibraryDestination(
3940
applicationNavigation = applicationNavigation,
4041
)
4142

42-
activeFileDownloadsViewModel.loadActiveDownloads(destination.libraryId)
43+
DisposableEffect(destination) {
44+
val promise = activeFileDownloadsViewModel.loadActiveDownloads(destination.libraryId)
45+
46+
onDispose {
47+
promise.cancel()
48+
}
49+
}
4350
}
4451
}
4552

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/nowplaying/view/viewmodels/NowPlayingCoverArtViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import com.lasthopesoftware.bluewater.shared.messages.application.RegisterForApp
1616
import com.lasthopesoftware.bluewater.shared.messages.registerReceiver
1717
import com.lasthopesoftware.observables.LiftedInteractionState
1818
import com.lasthopesoftware.observables.MutableInteractionState
19-
import com.lasthopesoftware.observables.toMaybeObservable
19+
import com.lasthopesoftware.observables.toSingleObservable
2020
import com.lasthopesoftware.promises.extensions.toPromise
2121
import com.lasthopesoftware.resources.emptyByteArray
2222
import com.namehillsoftware.handoff.promises.Promise
@@ -52,7 +52,7 @@ class NowPlayingCoverArtViewModel(
5252
val isNowPlayingImageLoading = isNowPlayingImageLoadingState.asInteractionState()
5353
val nowPlayingImage = LiftedInteractionState(
5454
promisedDefaultImage
55-
.toMaybeObservable()
55+
.toSingleObservable()
5656
.toObservable()
5757
.concatWith(nowPlayingImageState.filter { it.value.isNotEmpty() }.map { it.value }),
5858
emptyByteArray)

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/playback/service/PlaybackService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ import com.lasthopesoftware.bluewater.shared.lazyLogger
9797
import com.lasthopesoftware.bluewater.shared.messages.application.ApplicationMessageBus.Companion.getApplicationMessageBus
9898
import com.lasthopesoftware.bluewater.shared.messages.application.getScopedMessageBus
9999
import com.lasthopesoftware.bluewater.shared.messages.registerReceiver
100-
import com.lasthopesoftware.observables.toMaybeObservable
100+
import com.lasthopesoftware.observables.toSingleObservable
101101
import com.lasthopesoftware.policies.retries.RetryOnRejectionLazyPromise
102102
import com.lasthopesoftware.promises.ForwardedResponse.Companion.forward
103103
import com.lasthopesoftware.promises.PromiseDelay.Companion.delay
@@ -927,7 +927,7 @@ import java.util.concurrent.TimeoutException
927927
val promisedPlayedFile = playingFile.promisePlayedFile()
928928
val localSubscription = trackPositionBroadcaster.run {
929929
Observable.interval(1, TimeUnit.SECONDS, lazyObservationScheduler.value)
930-
.flatMapMaybe { promisedPlayedFile.progress.toMaybeObservable() }
930+
.flatMapSingle { promisedPlayedFile.progress.toSingleObservable() }
931931
.distinctUntilChanged()
932932
.subscribe(observeUpdates(playingFile))
933933
}

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/job/StoredFileJobProcessor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import com.lasthopesoftware.bluewater.client.stored.library.items.files.job.exce
88
import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile
99
import com.lasthopesoftware.bluewater.client.stored.library.items.files.updates.UpdateStoredFiles
1010
import com.lasthopesoftware.bluewater.shared.lazyLogger
11-
import com.lasthopesoftware.observables.observeProgress
11+
import com.lasthopesoftware.observables.observeBufferedProgress
1212
import com.lasthopesoftware.policies.ratelimiting.PromisingRateLimiter
1313
import com.lasthopesoftware.promises.extensions.ProgressingPromiseProxy
1414
import com.lasthopesoftware.promises.extensions.toPromise
@@ -39,7 +39,7 @@ class StoredFileJobProcessor(
3939
.flatMap { (libraryId, _, storedFile) ->
4040
Observable
4141
.just(StoredFileJobStatus(storedFile, StoredFileJobState.Queued))
42-
.concatWith(rateLimiter.enqueueProgressingPromise { StoredFileDownloadPromise(libraryId, storedFile) }.observeProgress())
42+
.concatWith(rateLimiter.enqueueProgressingPromise { StoredFileDownloadPromise(libraryId, storedFile) }.observeBufferedProgress())
4343
}
4444
}
4545

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/view/ActiveFileDownloadsView.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import com.lasthopesoftware.bluewater.client.browsing.files.list.TrackTitleItemV
7878
import com.lasthopesoftware.bluewater.client.browsing.files.list.ViewFileItem
7979
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListContentType
8080
import com.lasthopesoftware.bluewater.client.playback.nowplaying.view.ScreenDimensionsScope
81+
import com.lasthopesoftware.bluewater.client.stored.library.items.files.job.StoredFileJobState
8182
import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile
8283
import com.lasthopesoftware.bluewater.client.stored.library.sync.SyncIcon
8384
import com.lasthopesoftware.bluewater.shared.android.viewmodels.PooledCloseablesViewModel
@@ -141,9 +142,9 @@ private fun SyncMenu(
141142
fun RenderTrackHeaderItem(
142143
activeFileDownloadsViewModel: ActiveFileDownloadsViewModel,
143144
trackHeadlineViewModelProvider: PooledCloseablesViewModel<ViewFileItem>,
144-
storedFile: StoredFile
145+
storedFile: StoredFile,
146+
isActive: Boolean
145147
) {
146-
val downloadingFileId by activeFileDownloadsViewModel.downloadingFileId.subscribeAsState()
147148
val fileItemViewModel = remember(trackHeadlineViewModelProvider::getViewModel)
148149

149150
DisposableEffect(storedFile.serviceId) {
@@ -160,7 +161,7 @@ fun RenderTrackHeaderItem(
160161

161162
TrackTitleItemView(
162163
itemName = fileName,
163-
isActive = downloadingFileId == storedFile.id,
164+
isActive = isActive,
164165
)
165166
}
166167

@@ -171,7 +172,7 @@ fun DownloadingFilesList(
171172
modifier: Modifier = Modifier,
172173
headerHeight: Dp = 0.dp,
173174
) {
174-
val files by activeFileDownloadsViewModel.downloadingFiles.subscribeAsState()
175+
val files by activeFileDownloadsViewModel.syncingFiles.subscribeAsState()
175176
val lazyListState = rememberLazyListState()
176177

177178
LazyColumn(
@@ -199,12 +200,13 @@ fun DownloadingFilesList(
199200

200201
itemsIndexed(
201202
files,
202-
{ _, f -> f.id },
203-
contentType = { _, _ -> ItemListContentType.File }) { i, f ->
203+
{ _, (f, _) -> f.id },
204+
contentType = { _, _ -> ItemListContentType.File }) { i, (f, s) ->
204205
RenderTrackHeaderItem(
205206
activeFileDownloadsViewModel,
206207
trackHeadlineViewModelProvider,
207208
f,
209+
s == StoredFileJobState.Downloading
208210
)
209211

210212
if (i < files.lastIndex)

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/items/files/view/ActiveFileDownloadsViewModel.kt

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
44
import com.lasthopesoftware.bluewater.client.browsing.TrackLoadedViewState
55
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
66
import com.lasthopesoftware.bluewater.client.stored.library.items.files.AccessStoredFiles
7+
import com.lasthopesoftware.bluewater.client.stored.library.items.files.job.StoredFileJobState
78
import com.lasthopesoftware.bluewater.client.stored.library.items.files.repository.StoredFile
89
import com.lasthopesoftware.bluewater.client.stored.sync.ScheduleSyncs
910
import com.lasthopesoftware.bluewater.client.stored.sync.StoredFileMessage
@@ -13,86 +14,105 @@ import com.lasthopesoftware.bluewater.shared.messages.registerReceiver
1314
import com.lasthopesoftware.observables.LiftedInteractionState
1415
import com.lasthopesoftware.observables.MutableInteractionState
1516
import com.lasthopesoftware.observables.mapNotNull
17+
import com.lasthopesoftware.promises.extensions.regardless
18+
import com.lasthopesoftware.promises.extensions.toPromise
1619
import com.namehillsoftware.handoff.promises.Promise
20+
import java.util.LinkedList
21+
import java.util.concurrent.atomic.AtomicReference
1722

1823
class ActiveFileDownloadsViewModel(
1924
private val storedFileAccess: AccessStoredFiles,
2025
applicationMessages: RegisterForApplicationMessages,
2126
private val scheduler: ScheduleSyncs,
2227
) : ViewModel(), TrackLoadedViewState {
23-
private val mutableIsLoading = MutableInteractionState(false)
2428

29+
@Volatile
30+
private var isPartiallyUpdating = false
31+
private val lastPromisedFileUpdate = AtomicReference(Unit.toPromise())
32+
33+
private val mutableIsLoading = MutableInteractionState(false)
2534
private val mutableIsSyncing = MutableInteractionState(false)
2635
private val mutableIsSyncStateChangeEnabled = MutableInteractionState(false)
27-
private val mutableDownloadingFiles = MutableInteractionState(emptyMap<Int, StoredFile>())
28-
private val mutableDownloadingFileId = MutableInteractionState<Int?>(null)
29-
private val fileDownloadedRegistration = applicationMessages.registerReceiver { message: StoredFileMessage.FileDownloaded ->
30-
mutableDownloadingFiles.value -= message.storedFileId
31-
}
32-
33-
private val fileQueuedRegistration = applicationMessages.registerReceiver { message: StoredFileMessage.FileQueued ->
34-
message.storedFileId
35-
.takeUnless(mutableDownloadingFiles.value::containsKey)
36-
?.let(storedFileAccess::promiseStoredFile)
37-
?.then { storedFile ->
38-
if (storedFile != null && storedFile.libraryId == activeLibraryId?.id) {
39-
mutableDownloadingFiles.value += Pair(storedFile.id, storedFile)
40-
}
41-
}
42-
}
43-
44-
private val fileDownloadingRegistration = applicationMessages.registerReceiver { message: StoredFileMessage.FileDownloading ->
45-
mutableDownloadingFileId.value = message.storedFileId
46-
}
47-
48-
private val syncStartedReceiver = applicationMessages.registerReceiver { _ : SyncStateMessage.SyncStarted ->
49-
mutableIsSyncing.value = true
50-
}
51-
52-
private val syncStoppedReceiver = applicationMessages.registerReceiver { _ : SyncStateMessage.SyncStopped ->
53-
mutableIsSyncing.value = false
54-
}
36+
private val mutableSyncingFilesWithState = MutableInteractionState(emptyMap<Int, Pair<StoredFile, StoredFileJobState>>())
5537

5638
var activeLibraryId: LibraryId? = null
5739
private set
5840

5941
val isSyncing = mutableIsSyncing.asInteractionState()
6042
val isSyncStateChangeEnabled = mutableIsSyncStateChangeEnabled.asInteractionState()
61-
val downloadingFiles = LiftedInteractionState(
62-
mutableDownloadingFiles.mapNotNull().map { it.values.toList() },
43+
44+
val syncingFiles = LiftedInteractionState(
45+
mutableSyncingFilesWithState
46+
.mapNotNull()
47+
.filter { !isPartiallyUpdating }
48+
.map { m ->
49+
val downloadingArray = ArrayList<Pair<StoredFile, StoredFileJobState>>()
50+
val returnList = LinkedList<Pair<StoredFile, StoredFileJobState>>()
51+
val addedFiles = HashSet<Int>(m.values.size)
52+
53+
for ((k, p) in m) {
54+
val (f, s) = p
55+
if (!addedFiles.add(f.id)) continue
56+
if (s == StoredFileJobState.Downloading) downloadingArray.add(p)
57+
else returnList.add(p)
58+
}
59+
60+
returnList.addAll(0, downloadingArray)
61+
returnList
62+
},
6363
emptyList()
6464
)
65-
val downloadingFileId = mutableDownloadingFileId.asInteractionState()
65+
6666
override val isLoading = mutableIsLoading.asInteractionState()
6767

6868
init {
69+
addCloseable(applicationMessages.registerReceiver { message: StoredFileMessage.FileDownloaded ->
70+
mutableSyncingFilesWithState.value -= message.storedFileId
71+
})
72+
73+
addCloseable(applicationMessages.registerReceiver { message: StoredFileMessage.FileQueued ->
74+
updateStoredFileState(message.storedFileId, StoredFileJobState.Queued)
75+
})
76+
77+
addCloseable(applicationMessages.registerReceiver { message: StoredFileMessage.FileDownloading ->
78+
updateStoredFileState(message.storedFileId, StoredFileJobState.Downloading)
79+
})
80+
81+
addCloseable(applicationMessages.registerReceiver { message: StoredFileMessage.FileWriteError ->
82+
updateStoredFileState(message.storedFileId, StoredFileJobState.Queued)
83+
})
84+
85+
addCloseable(applicationMessages.registerReceiver { message: StoredFileMessage.FileReadError ->
86+
updateStoredFileState(message.storedFileId, StoredFileJobState.Queued)
87+
})
88+
89+
addCloseable(applicationMessages.registerReceiver { _ : SyncStateMessage.SyncStarted ->
90+
mutableIsSyncing.value = true
91+
})
92+
93+
addCloseable(applicationMessages.registerReceiver { _ : SyncStateMessage.SyncStopped ->
94+
mutableIsSyncing.value = false
95+
})
96+
6997
scheduler
7098
.promiseIsSyncing()
71-
.then { it ->
99+
.then {
72100
mutableIsSyncing.value = it
73101
mutableIsSyncStateChangeEnabled.value = true
74102
}
75103
}
76104

77-
override fun onCleared() {
78-
fileDownloadedRegistration.close()
79-
fileQueuedRegistration.close()
80-
syncStartedReceiver.close()
81-
syncStoppedReceiver.close()
82-
fileDownloadingRegistration.close()
83-
}
84-
85105
fun loadActiveDownloads(libraryId: LibraryId): Promise<*> {
86106
mutableIsLoading.value = true
87107
activeLibraryId = libraryId
88108
return storedFileAccess
89109
.promiseDownloadingFiles()
90110
.then { storedFiles ->
91-
mutableDownloadingFiles.value = storedFiles
92-
.filter { sf -> sf.libraryId == libraryId.id }
93-
.associateBy { sf -> sf.id }
111+
mutableSyncingFilesWithState.value = storedFiles
112+
.filter { sf -> sf.libraryId == libraryId.id }
113+
.associate { sf -> sf.id to (sf to StoredFileJobState.Queued) }
94114
}
95-
.must { _ -> mutableIsLoading.value = false }
115+
.must { _ -> mutableIsLoading.value = false }
96116
}
97117

98118
fun toggleSync() {
@@ -109,4 +129,26 @@ class ActiveFileDownloadsViewModel(
109129
mutableIsSyncStateChangeEnabled.value = true
110130
}
111131
}
132+
133+
private fun updateStoredFileState(storedFileId: Int, state: StoredFileJobState) {
134+
lastPromisedFileUpdate.getAndUpdate { prior ->
135+
prior.regardless {
136+
mutableSyncingFilesWithState.value[storedFileId]
137+
?.let { (storedFile, currentState) ->
138+
if (currentState != state) {
139+
isPartiallyUpdating = true
140+
mutableSyncingFilesWithState.value -= storedFile.id
141+
isPartiallyUpdating = false
142+
mutableSyncingFilesWithState.value += storedFile.id to (storedFile to state)
143+
}
144+
}
145+
?.toPromise()
146+
?: storedFileAccess.promiseStoredFile(storedFileId).then { storedFile ->
147+
if (storedFile != null && storedFile.libraryId == activeLibraryId?.id) {
148+
mutableSyncingFilesWithState.value += storedFile.id to (storedFile to state)
149+
}
150+
}
151+
}
152+
}
153+
}
112154
}

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/library/sync/LibrarySyncsHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class LibrarySyncsHandler(
4444
.flatMapMaybe { serviceFile ->
4545
storedFileUpdater
4646
.promiseStoredFileUpdate(libraryId, serviceFile)
47-
.then<StoredFileJob?>({ storedFile ->
47+
.then({ storedFile ->
4848
storedFile
4949
?.takeUnless { sf -> sf.isDownloadComplete }
5050
?.let { sf -> StoredFileJob(libraryId, serviceFile, sf) }

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/stored/sync/StoredFileMessage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package com.lasthopesoftware.bluewater.client.stored.sync
22

33
import com.lasthopesoftware.bluewater.shared.messages.application.ApplicationMessage
44

5-
interface StoredFileMessage : ApplicationMessage {
5+
sealed interface StoredFileMessage : ApplicationMessage {
66
val storedFileId: Int
77

88
class FileQueued(override val storedFileId: Int) : StoredFileMessage

projectBlueWater/src/main/java/com/lasthopesoftware/observables/MaybeObservePromise.kt

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)