Skip to content

feat: implement "Resume All" functionality for downloads#8165

Open
bermount wants to merge 4 commits intolibre-tube:masterfrom
bermount:resume-download
Open

feat: implement "Resume All" functionality for downloads#8165
bermount wants to merge 4 commits intolibre-tube:masterfrom
bermount:resume-download

Conversation

@bermount
Copy link
Copy Markdown
Contributor

@bermount bermount commented Feb 7, 2026

Adds a "Resume All" button to the downloads screen that allows users to simultaneously resume all incomplete downloads. This is managed by tracking incomplete download IDs in preferences and implementing a new service action that fills available download slots up to the maximum concurrence limit. The button is hidden when there is no incomplete download.
Works with #8161 .

Adds a "Resume All" button to the downloads screen that allows users to
simultaneously resume all incomplete downloads. This is managed by
tracking incomplete download IDs in preferences and implementing a new
service action that fills available download slots up to the maximum
concurrence limit.
Ensure that the download ID is tracked when manually resuming downloads.
app:layout_constraintBottom_toTopOf="@+id/delete_all"
app:layout_constraintEnd_toEndOf="parent"
tools:targetApi="o"
tools:visibility="visible" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think that this button is a bit unintuitive, i.e. the logo is just a download button and not really self-explainable.

Instead, I think it would look better to put the button in the same line as the sort order button.

I know this conflicts with the playlist name, but I think it's still the better solution. E.g. this could look like the following (tested locally):

diff --git a/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt
index 47123b199..fc38bbc78 100644
--- a/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt
+++ b/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt
@@ -231,6 +231,7 @@ class DownloadsFragmentPage : DynamicLayoutManagerFragment(R.layout.fragment_dow
                 }
 
                 binding.playlistName.text = playlist.downloadPlaylist.title
+                binding.playlistName.isVisible = true
 
                 playlist.downloadVideos.map { it.videoId }
             }
diff --git a/app/src/main/res/layout/fragment_download_content.xml b/app/src/main/res/layout/fragment_download_content.xml
index 151e3b0cf..bcb36d962 100644
--- a/app/src/main/res/layout/fragment_download_content.xml
+++ b/app/src/main/res/layout/fragment_download_content.xml
@@ -12,44 +12,64 @@
         android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <LinearLayout
+        <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:orientation="horizontal">
+            android:paddingHorizontal="8dp">
 
-            <TextView
-                android:id="@+id/playlist_name"
-                android:layout_width="0dp"
-                android:layout_weight="1"
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/resume_all"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:maxLines="1"
-                android:textSize="18sp"
-                android:textStyle="bold"
-                android:ellipsize="end"
-                android:paddingHorizontal="8dp"
-                tools:text="Downloaded Playlist Name"/>
+                android:text="@string/resume_all"
+                android:contentDescription="@string/resume_all"
+                app:icon="@drawable/ic_play"
+                android:tooltipText="@string/resume_all"
+                android:visibility="gone"
+                tools:targetApi="o"
+                tools:visibility="visible" />
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/sort_type"
                 style="?materialButtonTonalStyle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-            android:layout_marginEnd="8dp"
-            android:ellipsize="end"
+                android:layout_marginEnd="8dp"
+                android:ellipsize="end"
                 android:maxLines="1"
                 android:paddingHorizontal="10dp"
+                android:layout_alignParentEnd="true"
                 android:text="@string/sort_by"
                 app:icon="@drawable/ic_sort"
+                android:gravity="end"
                 app:iconGravity="textEnd" />
 
-        </LinearLayout>
+        </RelativeLayout>
+
+        <TextView
+            android:id="@+id/playlist_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:textSize="18sp"
+            android:textStyle="bold"
+            android:ellipsize="end"
+            android:paddingHorizontal="8dp"
+            android:visibility="gone"
+            tools:visibility="visible"
+            tools:text="Downloaded Playlist Name"/>
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/downloads_recView"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
-            android:orientation="vertical"/>
+            android:orientation="vertical" />
+
+        <androidx.fragment.app.FragmentContainerView
+            android:id="@+id/fragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
 
     </LinearLayout>
 
@@ -78,23 +98,6 @@
             android:textStyle="bold" />
     </LinearLayout>
 
-    <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/resume_all"
-        style="?attr/floatingActionButtonSmallSecondaryStyle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="end"
-        android:layout_marginEnd="16dp"
-        android:layout_marginBottom="8dp"
-        android:contentDescription="@string/resume"
-        android:src="@drawable/ic_download"
-        android:tooltipText="@string/resume"
-        android:visibility="gone"
-        app:layout_constraintBottom_toTopOf="@+id/delete_all"
-        app:layout_constraintEnd_toEndOf="parent"
-        tools:targetApi="o"
-        tools:visibility="visible" />
-
     <com.google.android.material.floatingactionbutton.FloatingActionButton
         android:id="@+id/delete_all"
         style="?attr/floatingActionButtonSmallSecondaryStyle"
@@ -127,9 +130,4 @@
         tools:targetApi="o"
         tools:visibility="visible" />
 
-    <androidx.fragment.app.FragmentContainerView
-        android:id="@+id/fragment"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0c6fb58cc..1f5ac9cce 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -543,6 +543,7 @@
     <string name="include_timestamp_in_filename">Include timestamp in filename</string>
     <string name="did_you_mean">Did you mean:</string>
     <string name="showing_results_for">Showing results for:</string>
+    <string name="resume_all">Resume all</string>
 
     <!-- Notification channel strings -->
     <string name="download_channel_name">Download Service</string>

Copy link
Copy Markdown
Contributor Author

@bermount bermount Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bnyro I agree that the icon alone is a bit unintuitive. However, as you mentioned, moving it to the top conflicts with the playlist name and makes the header look cluttered.

How about changing it to an button at the bottom with the text "Resume downloads"? I think this makes the action self-explainable while keeping the top layout clean.

68146846876

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't great, as it would move the location of the other buttons, thus making it somewhat surprising to the user.

I'm not a heavy download user, so I'm not quite sure what's the use case for this is, but if it's something that happens only very rarely, maybe it should be moved to a notification instead (e.g 'You have unfinished downloads' with a resume button?).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that a notification would be very annoying, a button makes more sense imo.

private fun resumeAll() {
lifecycleScope.launch(coroutineContext) {
// Get incomplete items directly using stored IDs
val incompleteIds = DownloadHelper.getIncompleteDownloadIds()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of storing the incomplete download IDs in the preferences, can't we just iterate over all download items in the database and compare their downloadSize to the actual file size?

Or otherwise we should store an isFinished attribute in the DownloadItem table, instead of putting that information into the preferences store.

@Bnyro
Copy link
Copy Markdown
Member

Bnyro commented Feb 19, 2026

Very good idea, thanks for the PR 👍

Comment on lines +77 to +81
return if (idsString.isBlank()) {
emptySet()
} else {
idsString.split(",").mapNotNull { it.toIntOrNull() }.toSet()
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return if (idsString.isBlank()) {
emptySet()
} else {
idsString.split(",").mapNotNull { it.toIntOrNull() }.toSet()
}
return idsString.split(",").mapNotNull { it.toIntOrNull() }.toSet()

split returns an empty list if the string is blank, which will be turned into an empty set

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants