Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
169eff8
Rewrite history fragment using Compose
Isira-Seneviratne Jan 14, 2025
77e254b
Remove unused classes
Isira-Seneviratne Jan 14, 2025
3d9394e
Reuse stream composables
Isira-Seneviratne Jan 14, 2025
f99fc13
Reuse ItemList composable
Isira-Seneviratne Jan 15, 2025
4ec5492
Update dependencies
Isira-Seneviratne Jan 15, 2025
38a533f
Combine ItemList code
Isira-Seneviratne Jan 15, 2025
3f1bd58
Rm unused file
Isira-Seneviratne Jan 15, 2025
48ad123
Implement clear history functionality
Isira-Seneviratne Jan 15, 2025
38645b0
Implement playback action buttons
Isira-Seneviratne Jan 20, 2025
fa98a92
Cache history in view model
Isira-Seneviratne Jan 20, 2025
6621b7f
Fix NPE
Isira-Seneviratne Jan 20, 2025
aeb4548
Add delete button for history items
Isira-Seneviratne Jan 21, 2025
3943a87
Fix preview issue
Isira-Seneviratne Jan 21, 2025
bf06923
Reuse dropdown composable
Isira-Seneviratne Jan 21, 2025
59a748d
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jan 24, 2025
21caa6c
Update Compose BOM
Isira-Seneviratne Jan 25, 2025
5abc03c
Extract thumbnail into common composable
Isira-Seneviratne Jan 27, 2025
6de5ce7
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jan 31, 2025
61bb81f
Fix black theme, remove manual background definition
Isira-Seneviratne Jan 31, 2025
212c678
Remove manual text color specification
Isira-Seneviratne Jan 31, 2025
ac34ada
Make DropdownTextMenuItem non-restartable
Isira-Seneviratne Feb 1, 2025
695c05b
Add confirmation dialog for clearing watch history
Isira-Seneviratne Feb 5, 2025
dbe3011
Remove Unknown class
Isira-Seneviratne Feb 9, 2025
491b433
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Feb 22, 2025
e877997
Improve constructors
Isira-Seneviratne Mar 6, 2025
975ba3c
Reduce button size, fix alignment
Isira-Seneviratne Mar 8, 2025
851ba4b
Use segmented button for sort options
Isira-Seneviratne Mar 11, 2025
033d288
Use "Sort By" label, remove outdated translations
Isira-Seneviratne Mar 16, 2025
2bafa2c
Add loading indicator in ItemList
Isira-Seneviratne Mar 24, 2025
54c2b49
Restore clear menu
Isira-Seneviratne Mar 25, 2025
a58b0da
Fix actions
Isira-Seneviratne Mar 25, 2025
e00e256
Rename .java to .kt
Isira-Seneviratne Mar 26, 2025
fe563f3
Improve DAO method calls
Isira-Seneviratne Mar 26, 2025
6f9af26
Update dependencies
Isira-Seneviratne Mar 26, 2025
287db90
Sort using last played by default
Isira-Seneviratne Mar 31, 2025
451c89b
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Apr 10, 2025
27a29ae
Update Jetpack Compose and Room
Isira-Seneviratne Apr 17, 2025
eb6a03a
Merge branch 'refactor' into History-Compose
Isira-Seneviratne May 14, 2025
12a97c6
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jun 7, 2025
0e422a4
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jul 9, 2025
4eae345
Combine history ordered query methods into one
Isira-Seneviratne Jul 11, 2025
b0427ec
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jul 12, 2025
f89ee3b
Rename .java to .kt
Isira-Seneviratne Jul 12, 2025
eac0dcd
Run database checkpoint on IO coroutine dispatcher
Isira-Seneviratne Jul 12, 2025
8b2c614
Cleanup
Isira-Seneviratne Jul 12, 2025
fa75283
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jul 18, 2025
48d0ebf
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Aug 4, 2025
44538ce
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Aug 27, 2025
d7953d6
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Sep 7, 2025
5e88ac4
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Dec 12, 2025
51a749a
Merge branch 'refactor' into History-Compose
Isira-Seneviratne Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.rx3.await
import io.reactivex.rxjava3.core.Single
import java.io.IOException
import java.time.OffsetDateTime
Expand All @@ -22,6 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime

class FeedDAOTest {
private lateinit var db: AppDatabase
Expand Down Expand Up @@ -106,14 +111,10 @@ class FeedDAOTest {
)
}

private fun setupUnlinkDelete(time: String) {
private fun setupUnlinkDelete(time: String) = runBlocking(Dispatchers.IO) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
streamDAO.deleteOrphans().await()
}

private fun clearAndFillTables() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ abstract class FeedDAO {
AND s.upload_date <> max_upload_date))
"""
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
abstract suspend fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)

@Query(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
abstract fun getStream(serviceId: Long, url: String): Maybe<StreamEntity>

@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable

@Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertInternal(stream: StreamEntity): Long

Expand Down Expand Up @@ -122,7 +119,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
WHERE f.stream_id = streams.uid)
"""
)
abstract fun deleteOrphans(): Int
abstract fun deleteOrphans(): Completable

/**
* Minimal entry class used when comparing/updating an existent stream.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@
import org.schabi.newpipe.local.holder.LocalPlaylistStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder;
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder;
import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder;
Expand Down Expand Up @@ -65,10 +62,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
private static final int HEADER_TYPE = 0;
private static final int FOOTER_TYPE = 1;

private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
private static final int STREAM_STATISTICS_GRID_HOLDER_TYPE = 0x1002;
private static final int STREAM_STATISTICS_CARD_HOLDER_TYPE = 0x1003;
private static final int STREAM_PLAYLIST_GRID_HOLDER_TYPE = 0x1004;
private static final int STREAM_PLAYLIST_CARD_HOLDER_TYPE = 0x1005;

Expand Down Expand Up @@ -293,14 +287,6 @@ public int getItemViewType(int position) {
} else {
return STREAM_PLAYLIST_HOLDER_TYPE;
}
case STATISTIC_STREAM_ITEM:
if (itemViewMode == ItemViewMode.CARD) {
return STREAM_STATISTICS_CARD_HOLDER_TYPE;
} else if (itemViewMode == ItemViewMode.GRID) {
return STREAM_STATISTICS_GRID_HOLDER_TYPE;
} else {
return STREAM_STATISTICS_HOLDER_TYPE;
}
default:
Log.e(TAG, "No holder type has been considered for item: ["
+ item.getLocalItemType() + "]");
Expand All @@ -316,43 +302,36 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup paren
Log.d(TAG, "onCreateViewHolder() called with: "
+ "parent = [" + parent + "], type = [" + type + "]");
}
switch (type) {
case HEADER_TYPE:
return new HeaderFooterHolder(header);
case FOOTER_TYPE:
return new HeaderFooterHolder(footer);
case LOCAL_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistCardItemHolder(localItemBuilder, parent);
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE:
return new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE:
return new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE:
return new RemotePlaylistCardItemHolder(localItemBuilder, parent);
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE:
return new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE:
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE:
return new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_CARD_HOLDER_TYPE:
return new LocalPlaylistStreamCardItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_HOLDER_TYPE:
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_GRID_HOLDER_TYPE:
return new LocalStatisticStreamGridItemHolder(localItemBuilder, parent);
case STREAM_STATISTICS_CARD_HOLDER_TYPE:
return new LocalStatisticStreamCardItemHolder(localItemBuilder, parent);
default:
return switch (type) {
case HEADER_TYPE -> new HeaderFooterHolder(header);
case FOOTER_TYPE -> new HeaderFooterHolder(footer);
case LOCAL_PLAYLIST_HOLDER_TYPE ->
new LocalPlaylistItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_GRID_HOLDER_TYPE ->
new LocalPlaylistGridItemHolder(localItemBuilder, parent);
case LOCAL_PLAYLIST_CARD_HOLDER_TYPE ->
new LocalPlaylistCardItemHolder(localItemBuilder, parent);
case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE ->
new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_HOLDER_TYPE ->
new RemotePlaylistItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_GRID_HOLDER_TYPE ->
new RemotePlaylistGridItemHolder(localItemBuilder, parent);
case REMOTE_PLAYLIST_CARD_HOLDER_TYPE ->
new RemotePlaylistCardItemHolder(localItemBuilder, parent);
case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE ->
new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_HOLDER_TYPE ->
new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_GRID_HOLDER_TYPE ->
new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent);
case STREAM_PLAYLIST_CARD_HOLDER_TYPE ->
new LocalPlaylistStreamCardItemHolder(localItemBuilder, parent);
default -> {
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
return new FallbackViewHolder(new View(parent.getContext()));
}
yield new FallbackViewHolder(new View(parent.getContext()));
}
};
}

@SuppressWarnings("FinalParameters")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.schabi.newpipe.local.feed

import android.content.Context
import android.util.Log
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
Expand All @@ -11,6 +10,7 @@ import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.MainActivity.DEBUG
import kotlinx.coroutines.rx3.await
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
Expand Down Expand Up @@ -110,20 +110,9 @@ class FeedDatabaseManager(context: Context) {
)
}

fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
suspend fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) {
feedTable.unlinkStreamsOlderThan(oldestAllowedDate)
streamTable.deleteOrphans()
}

fun clear() {
feedTable.deleteAll()
val deletedOrphans = streamTable.deleteOrphans()
if (DEBUG) {
Log.d(
this::class.java.simpleName,
"clear() → streamTable.deleteOrphans() → $deletedOrphans"
)
}
streamTable.deleteOrphans().await()
}

// /////////////////////////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Notification
import io.reactivex.rxjava3.core.Single
Expand All @@ -15,6 +14,8 @@ import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.rx3.rxCompletable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
Expand Down Expand Up @@ -261,7 +262,7 @@ class FeedLoadManager(private val context: Context) {
* Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE].
* Remove streams from the database which are not linked / used by any table.
*/
private fun postProcessFeed() = Completable.fromRunnable {
private fun postProcessFeed() = rxCompletable(Dispatchers.IO) {
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message))
feedDatabaseManager.removeOrphansOrOlderStreams()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.schabi.newpipe.local.history

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.Surface
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.ui.screens.HistoryScreen
import org.schabi.newpipe.ui.theme.AppTheme

class HistoryFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = content {
AppTheme {
Surface {
HistoryScreen()
}
}
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
val context = requireActivity()
(context as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.title_activity_history)

val recordManager = HistoryRecordManager(context)
context.addMenuProvider(
object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_history, menu)
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.action_history_clear -> {
AlertDialog.Builder(context)
.setTitle(R.string.delete_view_history_alert)
.setNegativeButton(R.string.cancel) { dialog, which -> dialog.dismiss() }
.setPositiveButton(R.string.delete) { dialog, which ->
viewLifecycleOwner.lifecycleScope.launch {
launch(getExceptionHandler("Delete playback states")) {
recordManager.deleteCompleteStreamStateHistory().await()
Toast
.makeText(context, R.string.watch_history_states_deleted, Toast.LENGTH_SHORT)
.show()
}

launch(getExceptionHandler("Delete watch history")) {
recordManager.deleteWholeStreamHistory().await()
Toast.makeText(context, R.string.watch_history_deleted, Toast.LENGTH_SHORT)
.show()
}

launch(getExceptionHandler("Clear orphaned records")) {
recordManager.removeOrphanedRecords().await()
}
}
}
.show()
}
}
return true
}
},
viewLifecycleOwner
)
}

private fun getExceptionHandler(action: String) = CoroutineExceptionHandler { _, throwable ->
openActivity(requireContext(), ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, action))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,30 +151,22 @@ public Maybe<Long> onViewed(final StreamInfo info) {
}

public Completable deleteStreamHistoryAndState(final long streamId) {
return Completable.fromAction(() -> {
streamStateTable.deleteState(streamId);
streamHistoryTable.deleteStreamHistory(streamId);
}).subscribeOn(Schedulers.io());
return streamStateTable.deleteState(streamId)
.andThen(streamHistoryTable.deleteStreamHistory(streamId));
}

public Single<Integer> deleteWholeStreamHistory() {
return Single.fromCallable(streamHistoryTable::deleteAll)
.subscribeOn(Schedulers.io());
public Completable deleteWholeStreamHistory() {
return streamHistoryTable.deleteAll().subscribeOn(Schedulers.io());
}

public Single<Integer> deleteCompleteStreamStateHistory() {
return Single.fromCallable(streamStateTable::deleteAll)
.subscribeOn(Schedulers.io());
public Completable deleteCompleteStreamStateHistory() {
return streamStateTable.deleteAll().subscribeOn(Schedulers.io());
}

public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
}

public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
}

private boolean isStreamHistoryEnabled() {
return sharedPreferences.getBoolean(streamHistoryKey, false);
}
Expand Down Expand Up @@ -283,8 +275,7 @@ public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
// Utility
///////////////////////////////////////////////////////

public Single<Integer> removeOrphanedRecords() {
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
public Completable removeOrphanedRecords() {
return streamTable.deleteOrphans().subscribeOn(Schedulers.io());
}

}
Loading
Loading