diff --git a/app/build.gradle b/app/build.gradle index 0841086ad7c..01591267c9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -232,6 +232,7 @@ dependencies { implementation libs.androidx.recyclerview implementation libs.androidx.room.runtime implementation libs.androidx.room.rxjava3 + implementation libs.androidx.room.paging kapt libs.androidx.room.compiler implementation libs.androidx.swiperefreshlayout implementation libs.androidx.work.runtime diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index a34cfece671..04e5552e028 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -129,7 +129,7 @@ class DatabaseMigrationTest { ) val migratedDatabaseV3 = getMigratedDatabase() - val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() + val listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst() // Only expect 2, the one with the null url will be ignored assertEquals(2, listFromDB.size) diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt index 893ae82b7f9..8c75c438f6e 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt @@ -3,7 +3,9 @@ package org.schabi.newpipe.database import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider -import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx3.await import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -22,7 +24,6 @@ import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.stream.StreamType import java.io.IOException import java.time.OffsetDateTime -import kotlin.streams.toList class FeedDAOTest { private lateinit var db: AppDatabase @@ -94,14 +95,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() { diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt index c392d8d3d66..ce3aeb84ac0 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt @@ -72,6 +72,6 @@ class LocalPlaylistManagerTest { val result = manager.createPlaylist("name", listOf(stream, upserted)) result.test().await().assertComplete() - database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted)) + database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted)) } } diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index e7ed934977a..1c4c8129c4c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -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( """ diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 150d4a8e5b5..57058426968 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -1,18 +1,5 @@ package org.schabi.newpipe.database.history.dao; -import androidx.annotation.Nullable; -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; - -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; @@ -25,58 +12,85 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; -@Dao -public abstract class StreamHistoryDAO implements HistoryDAO { - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE - + " WHERE " + STREAM_ACCESS_DATE + " = " - + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") - @Override - @Nullable - public abstract StreamHistoryEntity getLatestEntry(); +import androidx.annotation.Nullable; +import androidx.paging.PagingSource; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.RewriteQueriesToDropUnusedColumns; - @Override - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) - public abstract Flowable> getAll(); +import org.schabi.newpipe.database.history.model.StreamHistoryEntity; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; +import org.schabi.newpipe.database.stream.StreamStatisticsEntry; - @Override - @Query("DELETE FROM " + STREAM_HISTORY_TABLE) - public abstract int deleteAll(); +import java.util.List; - @Override - public Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; - @Query("SELECT * FROM " + STREAM_TABLE - + " INNER JOIN " + STREAM_HISTORY_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") - public abstract Flowable> getHistory(); +@Dao +public interface StreamHistoryDAO { + @Insert + long insert(StreamHistoryEntity entity); + @Delete + void delete(StreamHistoryEntity entity); + + @Query("DELETE FROM " + STREAM_HISTORY_TABLE) + Completable deleteAll(); @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + STREAM_HISTORY_TABLE + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + " ORDER BY " + STREAM_ID + " ASC") - public abstract Flowable> getHistorySortedById(); + Flowable> getHistorySortedById(); @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") @Nullable - public abstract StreamHistoryEntity getLatestEntry(long streamId); + StreamHistoryEntity getLatestEntry(long streamId); @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteStreamHistory(long streamId); + Completable deleteStreamHistory(long streamId); + + @Query("SELECT * FROM " + STREAM_TABLE + + " INNER JOIN " + STREAM_HISTORY_TABLE + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") + Flowable> getHistory(); @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM " + STREAM_TABLE + // Select the latest entry and watch count for each stream id on history table + + " INNER JOIN " + + "(SELECT " + JOIN_STREAM_ID + ", " + + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + + " FROM " + STREAM_HISTORY_TABLE + + " GROUP BY " + JOIN_STREAM_ID + ")" + + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + " LEFT JOIN " + + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + + STREAM_PROGRESS_MILLIS + + " FROM " + STREAM_STATE_TABLE + " )" + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS + + + " ORDER BY " + STREAM_LATEST_DATE + " DESC" + ) + PagingSource getHistoryOrderedByLastWatched(); + + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM " + STREAM_TABLE // Select the latest entry and watch count for each stream id on history table + " INNER JOIN " + "(SELECT " + JOIN_STREAM_ID + ", " + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT - + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" + + " FROM " + STREAM_HISTORY_TABLE + + " GROUP BY " + JOIN_STREAM_ID + ")" + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID @@ -84,6 +98,9 @@ public Flowable> listByService(final int serviceId) { + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + STREAM_PROGRESS_MILLIS + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) - public abstract Flowable> getStatistics(); + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS + + + " ORDER BY " + STREAM_WATCH_COUNT + " DESC" + ) + PagingSource getHistoryOrderedByViewCount(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index 1f3654e7ae4..c3757730676 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -6,8 +6,6 @@ import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS -import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.util.image.ImageStrategy import java.time.OffsetDateTime class StreamStatisticsEntry( @@ -26,16 +24,6 @@ class StreamStatisticsEntry( @ColumnInfo(name = STREAM_WATCH_COUNT) val watchCount: Long ) : LocalItem { - fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - override fun getLocalItemType(): LocalItem.LocalItemType { return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt index 0015c8e0aaa..df127c8b354 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -30,9 +30,6 @@ abstract class StreamDAO : BasicDAO { @Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId") abstract fun getStream(serviceId: Long, url: String): Maybe - @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 @@ -123,7 +120,7 @@ abstract class StreamDAO : BasicDAO { WHERE f.stream_id = streams.uid) """ ) - abstract fun deleteOrphans(): Int + abstract fun deleteOrphans(): Completable /** * Minimal entry class used when comparing/updating an existent stream. diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java deleted file mode 100644 index 6f1ecf173d8..00000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.schabi.newpipe.database.stream.dao; - -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Maybe; - -@Dao -public interface StreamStateDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + STREAM_STATE_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_STATE_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - Maybe getState(long streamId); - - @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - int deleteState(long streamId); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - void silentInsertInternal(StreamStateEntity streamState); - - @Transaction - default long upsert(final StreamStateEntity stream) { - silentInsertInternal(stream); - return update(stream); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt new file mode 100644 index 00000000000..ab6bea515e4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt @@ -0,0 +1,23 @@ +package org.schabi.newpipe.database.stream.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Maybe +import org.schabi.newpipe.database.stream.model.StreamStateEntity + +@Dao +interface StreamStateDAO { + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE) + fun deleteAll(): Completable + + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun getState(streamId: Long): Maybe + + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun deleteState(streamId: Long): Completable + + @Upsert + fun upsert(stream: StreamStateEntity) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index b33619dea7a..b129d18a1fc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -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; @@ -65,10 +62,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter 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") diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index ed65d4048e8..9eb381666dd 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -1,13 +1,12 @@ 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 import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.schedulers.Schedulers -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 @@ -111,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() } // ///////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt index 9b0f177d568..62f827d73c1 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadManager.kt @@ -4,13 +4,14 @@ 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 import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.processors.PublishProcessor import io.reactivex.rxjava3.schedulers.Schedulers +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 @@ -256,7 +257,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() diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java deleted file mode 100644 index 709a16b68b6..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.schabi.newpipe.local.history; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import org.schabi.newpipe.util.Localization; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; - - -/** - * This is an adapter for history entries. - * - * @param the type of the entries - * @param the type of the view holder - */ -public abstract class HistoryEntryAdapter - extends RecyclerView.Adapter { - private final ArrayList mEntries; - private final DateFormat mDateFormat; - private final Context mContext; - private OnHistoryItemClickListener onHistoryItemClickListener = null; - - public HistoryEntryAdapter(final Context context) { - super(); - mContext = context; - mEntries = new ArrayList<>(); - mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, - Localization.getPreferredLocale(context)); - } - - public void setEntries(@NonNull final Collection historyEntries) { - mEntries.clear(); - mEntries.addAll(historyEntries); - notifyDataSetChanged(); - } - - public Collection getItems() { - return mEntries; - } - - public void clear() { - mEntries.clear(); - notifyDataSetChanged(); - } - - protected String getFormattedDate(final Date date) { - return mDateFormat.format(date); - } - - protected String getFormattedViewString(final long viewCount) { - return Localization.shortViewCount(mContext, viewCount); - } - - @Override - public int getItemCount() { - return mEntries.size(); - } - - @Override - public void onBindViewHolder(final VH holder, final int position) { - final E entry = mEntries.get(position); - holder.itemView.setOnClickListener(v -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemClick(entry); - } - }); - - holder.itemView.setOnLongClickListener(view -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemLongClick(entry); - return true; - } - return false; - }); - - onBindViewHolder(holder, entry, position); - } - - @Override - public void onViewRecycled(@NonNull final VH holder) { - super.onViewRecycled(holder); - holder.itemView.setOnClickListener(null); - } - - abstract void onBindViewHolder(VH holder, E entry, int position); - - public void setOnHistoryItemClickListener( - @Nullable final OnHistoryItemClickListener onHistoryItemClickListener) { - this.onHistoryItemClickListener = onHistoryItemClickListener; - } - - public boolean isEmpty() { - return mEntries.isEmpty(); - } - - public interface OnHistoryItemClickListener { - void onHistoryItemClick(E item); - - void onHistoryItemLongClick(E item); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt new file mode 100644 index 00000000000..b505331dd95 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -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)) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index f2fdf9eba63..09cc193b5b9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -151,30 +151,22 @@ public Maybe 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 deleteWholeStreamHistory() { - return Single.fromCallable(streamHistoryTable::deleteAll) - .subscribeOn(Schedulers.io()); + public Completable deleteWholeStreamHistory() { + return streamHistoryTable.deleteAll().subscribeOn(Schedulers.io()); } - public Single deleteCompleteStreamStateHistory() { - return Single.fromCallable(streamStateTable::deleteAll) - .subscribeOn(Schedulers.io()); + public Completable deleteCompleteStreamStateHistory() { + return streamStateTable.deleteAll().subscribeOn(Schedulers.io()); } public Flowable> getStreamHistorySortedById() { return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); } - public Flowable> getStreamStatistics() { - return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); - } - private boolean isStreamHistoryEnabled() { return sharedPreferences.getBoolean(streamHistoryKey, false); } @@ -283,8 +275,7 @@ public Single> loadLocalStreamStateBatch( // Utility /////////////////////////////////////////////////////// - public Single removeOrphanedRecords() { - return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + public Completable removeOrphanedRecords() { + return streamTable.deleteOrphans().subscribeOn(Schedulers.io()); } - } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt new file mode 100644 index 00000000000..84f8e3b47e0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -0,0 +1,45 @@ +package org.schabi.newpipe.local.history + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.map +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.ui.components.items.Stream + +class HistoryViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle, +) : AndroidViewModel(application) { + private val historyDao = NewPipeDatabase.getInstance(getApplication()).streamHistoryDAO() + + val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.LAST_PLAYED) + val historyItems = sortKey + .flatMapLatest { + Pager(PagingConfig(pageSize = 20)) { + when (it) { + SortKey.LAST_PLAYED -> historyDao.getHistoryOrderedByLastWatched() + SortKey.MOST_PLAYED -> historyDao.getHistoryOrderedByViewCount() + } + }.flow + } + .map { pagingData -> pagingData.map { Stream(it) } } + .flowOn(Dispatchers.IO) + .cachedIn(viewModelScope) + + fun updateOrder(sortKey: SortKey) { + savedStateHandle[ORDER_KEY] = sortKey + } + + companion object { + private const val ORDER_KEY = "order" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt new file mode 100644 index 00000000000..b4bb6645dd6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt @@ -0,0 +1,9 @@ +package org.schabi.newpipe.local.history + +import androidx.annotation.StringRes +import org.schabi.newpipe.R + +enum class SortKey(@StringRes val title: Int) { + LAST_PLAYED(R.string.history_sort_date), + MOST_PLAYED(R.string.history_sort_views) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java deleted file mode 100644 index 3302e387ec5..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ /dev/null @@ -1,392 +0,0 @@ -package org.schabi.newpipe.local.history; - -import android.content.Context; -import android.os.Bundle; -import android.os.Parcelable; -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.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.viewbinding.ViewBinding; - -import com.evernote.android.state.State; -import com.google.android.material.snackbar.Snackbar; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.databinding.PlaylistControlBinding; -import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; -import org.schabi.newpipe.info_list.dialog.InfoItemDialog; -import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; -import org.schabi.newpipe.local.BaseLocalListFragment; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.settings.HistorySettingsFragment; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.PlayButtonHelper; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; - -public class StatisticsPlaylistFragment - extends BaseLocalListFragment, Void> - implements PlaylistControlViewHolder { - private final CompositeDisposable disposables = new CompositeDisposable(); - @State - Parcelable itemsListState; - private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; - - private StatisticPlaylistControlBinding headerBinding; - private PlaylistControlBinding playlistControlBinding; - - /* Used for independent events */ - private Subscription databaseSubscription; - private HistoryRecordManager recordManager; - - private List processResult(final List results) { - final Comparator comparator; - switch (sortMode) { - case LAST_PLAYED: - comparator = Comparator.comparing(StreamStatisticsEntry::getLatestAccessDate); - break; - case MOST_PLAYED: - comparator = Comparator.comparingLong(StreamStatisticsEntry::getWatchCount); - break; - default: - return null; - } - Collections.sort(results, comparator.reversed()); - return results; - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Creation - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - recordManager = new HistoryRecordManager(getContext()); - } - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_playlist, container, false); - } - - @Override - public void onResume() { - super.onResume(); - if (activity != null) { - setTitle(activity.getString(R.string.title_activity_history)); - } - } - - @Override - public void onCreateOptionsMenu(@NonNull final Menu menu, - @NonNull final MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_history, menu); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Views - /////////////////////////////////////////////////////////////////////////// - - @Override - protected void initViews(final View rootView, final Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - if (!useAsFrontPage) { - setTitle(getString(R.string.title_last_played)); - } - } - - @Override - protected ViewBinding getListHeader() { - headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(), - itemsList, false); - playlistControlBinding = headerBinding.playlistControl; - - return headerBinding; - } - - @Override - protected void initListeners() { - super.initListeners(); - - itemListAdapter.setSelectedListener(new OnClickGesture<>() { - @Override - public void selected(final LocalItem selectedItem) { - if (selectedItem instanceof StreamStatisticsEntry) { - final StreamEntity item = - ((StreamStatisticsEntry) selectedItem).getStreamEntity(); - NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), - item.getServiceId(), item.getUrl(), item.getTitle(), null, false); - } - } - - @Override - public void held(final LocalItem selectedItem) { - if (selectedItem instanceof StreamStatisticsEntry) { - showInfoItemDialog((StreamStatisticsEntry) selectedItem); - } - } - }); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_history_clear) { - HistorySettingsFragment - .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); - } else { - return super.onOptionsItemSelected(item); - } - return true; - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Loading - /////////////////////////////////////////////////////////////////////////// - - @Override - public void startLoading(final boolean forceLoad) { - super.startLoading(forceLoad); - recordManager.getStreamStatistics() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryObserver()); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Destruction - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onPause() { - super.onPause(); - itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - if (itemListAdapter != null) { - itemListAdapter.unsetSelectedListener(); - } - - headerBinding = null; - playlistControlBinding = null; - - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - databaseSubscription = null; - } - - @Override - public void onDestroy() { - super.onDestroy(); - recordManager = null; - itemsListState = null; - } - - /////////////////////////////////////////////////////////////////////////// - // Statistics Loader - /////////////////////////////////////////////////////////////////////////// - - private Subscriber> getHistoryObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(final Subscription s) { - showLoading(); - - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(final List streams) { - handleResult(streams); - if (databaseSubscription != null) { - databaseSubscription.request(1); - } - } - - @Override - public void onError(final Throwable exception) { - showError( - new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics")); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull final List result) { - super.handleResult(result); - if (itemListAdapter == null) { - return; - } - - playlistControlBinding.getRoot().setVisibility(View.VISIBLE); - - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - itemListAdapter.addItems(processResult(result)); - if (itemsListState != null && itemsList.getLayoutManager() != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - - PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this); - - headerBinding.sortButton.setOnClickListener(view -> toggleSortMode()); - - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected void resetFragment() { - super.resetFragment(); - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void toggleSortMode() { - if (sortMode == StatisticSortMode.LAST_PLAYED) { - sortMode = StatisticSortMode.MOST_PLAYED; - setTitle(getString(R.string.title_most_played)); - headerBinding.sortButtonIcon.setImageResource(R.drawable.ic_history); - headerBinding.sortButtonText.setText(R.string.title_last_played); - } else { - sortMode = StatisticSortMode.LAST_PLAYED; - setTitle(getString(R.string.title_last_played)); - headerBinding.sortButtonIcon.setImageResource( - R.drawable.ic_filter_list); - headerBinding.sortButtonText.setText(R.string.title_most_played); - } - startLoading(true); - } - - private PlayQueue getPlayQueueStartingAt(final StreamStatisticsEntry infoItem) { - return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); - } - - private void showInfoItemDialog(final StreamStatisticsEntry item) { - final Context context = getContext(); - final StreamInfoItem infoItem = item.toStreamInfoItem(); - - try { - final InfoItemDialog.Builder dialogBuilder = - new InfoItemDialog.Builder(getActivity(), context, this, infoItem); - - // set entries in the middle; the others are added automatically - dialogBuilder - .addEntry(StreamDialogDefaultEntry.DELETE) - .setAction( - StreamDialogDefaultEntry.DELETE, - (f, i) -> deleteEntry( - Math.max(itemListAdapter.getItemsList().indexOf(item), 0))) - .create() - .show(); - } catch (final IllegalArgumentException e) { - InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); - } - } - - private void deleteEntry(final int index) { - final LocalItem infoItem = itemListAdapter.getItemsList().get(index); - if (infoItem instanceof StreamStatisticsEntry) { - final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem; - final Disposable onDelete = recordManager - .deleteStreamHistoryAndState(entry.getStreamId()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - if (getView() != null) { - Snackbar.make(getView(), R.string.one_item_deleted, - Snackbar.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), - R.string.one_item_deleted, - Toast.LENGTH_SHORT).show(); - } - }, - throwable -> showSnackBarError(new ErrorInfo(throwable, - UserAction.DELETE_FROM_HISTORY, "Deleting item"))); - - disposables.add(onDelete); - } - } - - @Override - public PlayQueue getPlayQueue() { - return getPlayQueue(0); - } - - private PlayQueue getPlayQueue(final int index) { - if (itemListAdapter == null) { - return new SinglePlayQueue(Collections.emptyList(), 0); - } - - final List infoItems = itemListAdapter.getItemsList(); - final List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final LocalItem item : infoItems) { - if (item instanceof StreamStatisticsEntry) { - streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); - } - } - return new SinglePlayQueue(streamInfoItems, index); - } - - private enum StatisticSortMode { - LAST_PLAYED, - MOST_PLAYED, - } -} - diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java deleted file mode 100644 index 4e03d5fb105..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.ViewGroup; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.local.LocalItemBuilder; - -public class LocalStatisticStreamCardItemHolder extends LocalStatisticStreamItemHolder { - public LocalStatisticStreamCardItemHolder(final LocalItemBuilder infoItemBuilder, - final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_card_item, parent); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java deleted file mode 100644 index 39a43b0344f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.ViewGroup; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.local.LocalItemBuilder; - -public class LocalStatisticStreamGridItemHolder extends LocalStatisticStreamItemHolder { - public LocalStatisticStreamGridItemHolder(final LocalItemBuilder infoItemBuilder, - final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_grid_item, parent); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java deleted file mode 100644 index f26a76ad9f7..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.ktx.ViewUtils; -import org.schabi.newpipe.local.LocalItemBuilder; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.DependentPreferenceHelper; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.ServiceHelper; -import org.schabi.newpipe.util.image.CoilHelper; -import org.schabi.newpipe.views.AnimatedProgressBar; - -import java.time.format.DateTimeFormatter; -import java.util.concurrent.TimeUnit; - -/* - * Created by Christian Schabesberger on 01.08.16. - *

- * Copyright (C) Christian Schabesberger 2016 - * StreamInfoItemHolder.java is part of NewPipe. - *

- * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class LocalStatisticStreamItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; - public final TextView itemVideoTitleView; - public final TextView itemUploaderView; - public final TextView itemDurationView; - @Nullable - public final TextView itemAdditionalDetails; - private final AnimatedProgressBar itemProgressView; - - public LocalStatisticStreamItemHolder(final LocalItemBuilder itemBuilder, - final ViewGroup parent) { - this(itemBuilder, R.layout.list_stream_item, parent); - } - - LocalStatisticStreamItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, - final ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); - - itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); - itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); - itemDurationView = itemView.findViewById(R.id.itemDurationView); - itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); - itemProgressView = itemView.findViewById(R.id.itemProgressView); - } - - private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, - final DateTimeFormatter dateTimeFormatter) { - return Localization.concatenateStrings( - // watchCount - Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()), - dateTimeFormatter.format(entry.getLatestAccessDate()), - // serviceName - ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId())); - } - - @Override - public void updateFromItem(final LocalItem localItem, - final HistoryRecordManager historyRecordManager, - final DateTimeFormatter dateTimeFormatter) { - if (!(localItem instanceof StreamStatisticsEntry)) { - return; - } - final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - - itemVideoTitleView.setText(item.getStreamEntity().getTitle()); - itemUploaderView.setText(item.getStreamEntity().getUploader()); - - if (item.getStreamEntity().getDuration() > 0) { - itemDurationView. - setText(Localization.getDurationString(item.getStreamEntity().getDuration())); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), - R.color.duration_background_color)); - itemDurationView.setVisibility(View.VISIBLE); - - if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext()) - && item.getProgressMillis() > 0) { - itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.getStreamEntity().getDuration()); - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - } else { - itemProgressView.setVisibility(View.GONE); - } - } else { - itemDurationView.setVisibility(View.GONE); - itemProgressView.setVisibility(View.GONE); - } - - if (itemAdditionalDetails != null) { - itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateTimeFormatter)); - } - - // Default thumbnail is shown on error, while loading and if the url is empty - CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, - item.getStreamEntity().getThumbnailUrl()); - - itemView.setOnClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().selected(item); - } - }); - - itemView.setLongClickable(true); - itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().held(item); - } - return true; - }); - } - - @Override - public void updateState(final LocalItem localItem, - final HistoryRecordManager historyRecordManager) { - if (!(localItem instanceof StreamStatisticsEntry)) { - return; - } - final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - - if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext()) - && item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) { - itemProgressView.setMax((int) item.getStreamEntity().getDuration()); - if (itemProgressView.getVisibility() == View.VISIBLE) { - itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - } else { - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - ViewUtils.animate(itemProgressView, true, 500); - } - } else if (itemProgressView.getVisibility() == View.VISIBLE) { - ViewUtils.animate(itemProgressView, false, 500); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index 9bc9058c803..84a140247d9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -20,6 +20,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; public class HistorySettingsFragment extends BasePreferenceFragment { private String cacheWipeKey; @@ -79,8 +80,8 @@ private static Disposable getDeletePlaybackStatesDisposable( return recordManager.deleteCompleteStreamStateHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> Toast.makeText(context, - R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show(), + () -> Toast.makeText(context, R.string.watch_history_states_deleted, + Toast.LENGTH_SHORT).show(), throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Delete playback states"))); @@ -91,7 +92,7 @@ private static Disposable getWholeStreamHistoryDisposable( return recordManager.deleteWholeStreamHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> Toast.makeText(context, + () -> Toast.makeText(context, R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(), throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, @@ -103,7 +104,7 @@ private static Disposable getRemoveOrphanedRecordsDisposable( return recordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> { }, + Functions.EMPTY_ACTION, throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Clear orphaned records"))); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index 7e3f5d0c825..61db5fdd327 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java @@ -25,7 +25,7 @@ import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; -import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.history.HistoryFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.util.KioskTranslator; @@ -302,8 +302,8 @@ public int getTabIconRes(final Context context) { } @Override - public StatisticsPlaylistFragment getFragment(final Context context) { - return new StatisticsPlaylistFragment(); + public Fragment getFragment(final Context context) { + return new HistoryFragment(); } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt index 24421a93a75..b6d3fcc8900 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt @@ -6,13 +6,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp @@ -24,25 +20,18 @@ fun LicenseDialog(licenseHtml: AnnotatedString, onDismissRequest: () -> Unit) { val lazyListState = rememberLazyListState() ModalBottomSheet(onDismissRequest) { - CompositionLocalProvider( - // contentColorFor(MaterialTheme.colorScheme.containerColor), i.e. ModalBottomSheet's - // default background color, does not resolve correctly, so need to manually set the - // content color for MaterialTheme.colorScheme.background instead - LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) - ) { - LazyColumnThemedScrollbar(state = lazyListState) { - LazyColumn( - state = lazyListState - ) { - item { - if (licenseHtml.isEmpty()) { - LoadingIndicator(modifier = Modifier.padding(32.dp)) - } else { - Text( - text = licenseHtml, - modifier = Modifier.padding(horizontal = 12.dp), - ) - } + LazyColumnThemedScrollbar(state = lazyListState) { + LazyColumn( + state = lazyListState + ) { + item { + if (licenseHtml.isEmpty()) { + LoadingIndicator(modifier = Modifier.padding(32.dp)) + } else { + Text( + text = licenseHtml, + modifier = Modifier.padding(horizontal = 12.dp), + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt new file mode 100644 index 00000000000..0388c4b1852 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt @@ -0,0 +1,20 @@ +package org.schabi.newpipe.ui.components.common + +import androidx.annotation.StringRes +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.res.stringResource + +@Composable +@NonRestartableComposable +fun DropdownTextMenuItem( + @StringRes text: Int, + onClick: () -> Unit +) { + DropdownMenuItem( + text = { Text(text = stringResource(text)) }, + onClick = onClick + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt new file mode 100644 index 00000000000..f3a23f62f2b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt @@ -0,0 +1,52 @@ +package org.schabi.newpipe.ui.components.common + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun IconButtonWithLabel( + icon: ImageVector, + @StringRes label: Int, + onClick: () -> Unit, +) { + FilledTonalButton( + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), + onClick = onClick + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = icon, contentDescription = null) + Text(text = stringResource(label)) + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun IconButtonWithLabelPreview() { + AppTheme { + Surface { + IconButtonWithLabel(Icons.Default.Info, R.string.name) {} + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt new file mode 100644 index 00000000000..ee8f47d482b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt @@ -0,0 +1,43 @@ +package org.schabi.newpipe.ui.components.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.PlaylistPlay +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.PictureInPicture +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.ktx.findFragmentActivity +import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.util.NavigationHelper + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PlaybackControlButtons(queue: PlayQueue) { + val context = LocalContext.current + + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { + IconButtonWithLabel( + icon = Icons.Default.Headphones, + label = R.string.controls_background_title, + onClick = { NavigationHelper.playOnBackgroundPlayer(context, queue, false) }, + ) + + IconButtonWithLabel( + icon = Icons.AutoMirrored.Filled.PlaylistPlay, + label = R.string.play_all, + onClick = { NavigationHelper.playOnMainPlayer(context.findFragmentActivity(), queue) }, + ) + + IconButtonWithLabel( + icon = Icons.Default.PictureInPicture, + label = R.string.controls_popup_title, + onClick = { NavigationHelper.playOnPopupPlayer(context, queue, false) }, + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt new file mode 100644 index 00000000000..bbbff457da4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt @@ -0,0 +1,22 @@ +package org.schabi.newpipe.ui.components.items + +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.util.NO_SERVICE_ID + +sealed class Info + +class Playlist( + val serviceId: Int = NO_SERVICE_ID, + val url: String = "", + val name: String = "", + val thumbnails: List = emptyList(), + val uploaderName: String = "", + val streamCount: Long = 10, +) : Info() { + + constructor(item: PlaylistInfoItem) : this( + item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), + item.streamCount + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt index 4562e17aff7..d99dea9a121 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt @@ -1,48 +1,63 @@ package org.schabi.newpipe.ui.components.items +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems import androidx.preference.PreferenceManager import androidx.window.core.layout.WindowWidthSizeClass +import my.nanihadesuka.compose.LazyVerticalGridScrollbar import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.InfoItem -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem -import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar +import org.schabi.newpipe.ui.components.common.LoadingIndicator +import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem +import org.schabi.newpipe.ui.components.items.stream.StreamCardItem +import org.schabi.newpipe.ui.components.items.stream.StreamGridItem import org.schabi.newpipe.ui.components.items.stream.StreamListItem +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.util.DependentPreferenceHelper import org.schabi.newpipe.util.NavigationHelper @Composable fun ItemList( - items: List, + items: LazyPagingItems, mode: ItemViewMode = determineItemViewMode(), - listHeader: LazyListScope.() -> Unit = {} + header: @Composable () -> Unit = {}, ) { val context = LocalContext.current val onClick = remember { - { item: InfoItem -> + { item: Info -> val fragmentManager = context.findFragmentActivity().supportFragmentManager - if (item is StreamInfoItem) { + if (item is Stream) { NavigationHelper.openVideoDetailFragment( context, fragmentManager, item.serviceId, item.url, item.name, null, false ) - } else if (item is PlaylistInfoItem) { + } else if (item is Playlist) { NavigationHelper.openPlaylistFragment( fragmentManager, item.serviceId, item.url, item.name ) @@ -52,9 +67,9 @@ fun ItemList( // Handle long clicks for stream items // TODO: Adjust the menu display depending on where it was triggered - var selectedStream by remember { mutableStateOf(null) } + var selectedStream by rememberSaveable { mutableStateOf(null) } val onLongClick = remember { - { stream: StreamInfoItem -> + { stream: Stream -> selectedStream = stream } } @@ -67,25 +82,75 @@ fun ItemList( val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context) val nestedScrollModifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()) - if (mode == ItemViewMode.GRID) { - // TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar. + if (items.loadState.refresh is LoadState.NotLoading && items.itemCount == 0) { + EmptyStateComposable( + spec = EmptyStateSpec.NoVideos, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 128.dp) + ) + } else if (mode == ItemViewMode.GRID) { + val state = rememberLazyGridState() + + LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT + val minWidth = if (isCompact) 150.dp else 250.dp + + LazyVerticalGrid( + modifier = nestedScrollModifier, + state = state, + columns = GridCells.Adaptive(minWidth) + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + header() + } + + items(items.itemCount) { + val item = items[it] + val isSelected = selectedStream == item + + // TODO: Implement playlist and channel grid items. + if (item is Stream) { + StreamGridItem( + item, showProgress, isSelected, isCompact, onClick, onLongClick, + onDismissPopup + ) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.size(minWidth, if (isCompact) 150.dp else 200.dp)) + } + } + } + } } else { val state = rememberLazyListState() LazyColumnThemedScrollbar(state = state) { LazyColumn(modifier = nestedScrollModifier, state = state) { - listHeader() + item { + header() + } - items(items.size) { + items(items.itemCount) { val item = items[it] - if (item is StreamInfoItem) { + // TODO: Implement playlist and channel items. + if (item is Stream) { val isSelected = selectedStream == item - StreamListItem( - item, showProgress, isSelected, onClick, onLongClick, onDismissPopup - ) - } else if (item is PlaylistInfoItem) { + + if (mode == ItemViewMode.CARD) { + StreamCardItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup + ) + } else { + StreamListItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup + ) + } + } else if (item is Playlist) { PlaylistListItem(item, onClick) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.height(80.dp)) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt new file mode 100644 index 00000000000..dda3359fb3d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt @@ -0,0 +1,83 @@ +package org.schabi.newpipe.ui.components.items + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.schabi.newpipe.App +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.image.ImageStrategy +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.concurrent.TimeUnit + +@Parcelize +class Stream( + val serviceId: Int = NO_SERVICE_ID, + val url: String = "", + val name: String = "", + val thumbnails: List = emptyList(), + val uploaderName: String = "", + val type: StreamType, + val uploaderUrl: String? = null, + val duration: Long = TimeUnit.HOURS.toSeconds(1), + val detailText: String = "", + val streamId: Long = -1, +) : Info(), Parcelable { + + constructor(item: StreamInfoItem) : this( + item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), + item.streamType, item.uploaderUrl, item.duration, item.detailText + ) + + constructor(entry: StreamStatisticsEntry) : this( + entry.streamEntity.serviceId, entry.streamEntity.url, entry.streamEntity.title, + ImageStrategy.dbUrlToImageList(entry.streamEntity.thumbnailUrl), entry.streamEntity.uploader, + entry.streamEntity.streamType, entry.streamEntity.uploaderUrl, entry.streamEntity.duration, + entry.detailText, entry.streamId + ) + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(serviceId, url, name, type) + item.duration = duration + item.uploaderName = uploaderName + item.uploaderUrl = uploaderUrl + item.thumbnails = thumbnails + return item + } +} + +private val StreamInfoItem.detailText: String + get() { + val context = App.instance + val views = if (viewCount >= 0) { + when (streamType) { + StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount) + StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount) + else -> Localization.shortViewCount(context, viewCount) + } + } else { + "" + } + val date = Localization.relativeTimeOrTextual(context, uploadDate, textualUploadDate) + + return if (views.isEmpty()) { + date.orEmpty() + } else if (date.isNullOrEmpty()) { + views + } else { + "$views • $date" + } + } + +private val StreamStatisticsEntry.detailText: String + get() = + Localization.concatenateStrings( + Localization.shortViewCount(App.instance, watchCount), + DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(latestAccessDate), + ServiceHelper.getNameOfServiceById(streamEntity.serviceId), + ) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt new file mode 100644 index 00000000000..27c8915f294 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt @@ -0,0 +1,70 @@ +package org.schabi.newpipe.ui.components.items.common + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.util.image.ImageStrategy + +@Composable +fun Thumbnail( + images: List, + imageDescription: String, + @DrawableRes imagePlaceholder: Int, + cornerBackgroundColor: Color, + cornerIcon: ImageVector?, + cornerText: String, + contentScale: ContentScale, + modifier: Modifier = Modifier +) { + Box(contentAlignment = Alignment.BottomEnd) { + AsyncImage( + model = ImageStrategy.choosePreferredImage(images), + contentDescription = imageDescription, + placeholder = painterResource(imagePlaceholder), + error = painterResource(imagePlaceholder), + contentScale = contentScale, + modifier = modifier + ) + + Row( + modifier = Modifier + .padding(2.dp) + .background(cornerBackgroundColor) + .padding(2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (cornerIcon != null) { + Icon( + imageVector = cornerIcon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + } + + Text( + text = cornerText, + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt index 65388693573..73ac89f50af 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt @@ -17,15 +17,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.schabi.newpipe.extractor.InfoItem -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.ui.components.items.Playlist import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID @Composable fun PlaylistListItem( - playlist: PlaylistInfoItem, - onClick: (InfoItem) -> Unit = {}, + playlist: Playlist, + onClick: (Playlist) -> Unit = {}, ) { Row( modifier = Modifier @@ -49,7 +48,7 @@ fun PlaylistListItem( ) Text( - text = playlist.uploaderName.orEmpty(), + text = playlist.uploaderName, style = MaterialTheme.typography.bodySmall ) } @@ -60,8 +59,7 @@ fun PlaylistListItem( @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun PlaylistListItemPreview() { - val playlist = PlaylistInfoItem(NO_SERVICE_ID, "", "Playlist") - playlist.uploaderName = "Uploader" + val playlist = Playlist(NO_SERVICE_ID, "", "Playlist", uploaderName = "Uploader") AppTheme { Surface { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt index 36711105b27..e11c089aa40 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt @@ -1,66 +1,32 @@ package org.schabi.newpipe.ui.components.items.playlist -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage +import androidx.compose.ui.res.stringResource import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.ui.components.items.Playlist +import org.schabi.newpipe.ui.components.items.common.Thumbnail import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.image.ImageStrategy @Composable fun PlaylistThumbnail( - playlist: PlaylistInfoItem, + playlist: Playlist, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit ) { - Box(contentAlignment = Alignment.BottomEnd) { - AsyncImage( - model = ImageStrategy.choosePreferredImage(playlist.thumbnails), - contentDescription = null, - placeholder = painterResource(R.drawable.placeholder_thumbnail_playlist), - error = painterResource(R.drawable.placeholder_thumbnail_playlist), - contentScale = contentScale, - modifier = modifier - ) - - Row( - modifier = Modifier - .padding(2.dp) - .background(Color.Black.copy(alpha = 0.5f)) - .padding(2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.PlaylistPlay, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(18.dp) - ) - - val context = LocalContext.current - Text( - text = Localization.localizeStreamCountMini(context, playlist.streamCount), - color = Color.White, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 4.dp) - ) - } - } + Thumbnail( + images = playlist.thumbnails, + imageDescription = stringResource(R.string.playlist_content_description, playlist.name), + imagePlaceholder = R.drawable.placeholder_thumbnail_playlist, + cornerBackgroundColor = Color.Black.copy(alpha = 0.5f), + cornerIcon = Icons.AutoMirrored.Default.PlaylistPlay, + cornerText = Localization.localizeStreamCountMini(LocalContext.current, playlist.streamCount), + contentScale = contentScale, + modifier = modifier + ) } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt new file mode 100644 index 00000000000..67f2f9442fb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt @@ -0,0 +1,91 @@ +package org.schabi.newpipe.ui.components.items.stream + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.theme.AppTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StreamCardItem( + stream: Stream, + showProgress: Boolean, + isSelected: Boolean, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } + ) + .padding(top = 12.dp, start = 2.dp, end = 2.dp) + ) { + StreamThumbnail( + stream = stream, + showProgress = showProgress, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + + Column(modifier = Modifier.padding(10.dp)) { + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stream.uploaderName, + style = MaterialTheme.typography.bodySmall + ) + + Text( + text = stream.detailText, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StreamCardItemPreview( + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream +) { + AppTheme { + Surface { + StreamCardItem(stream, showProgress = false, isSelected = false) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt new file mode 100644 index 00000000000..3ae3b137989 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt @@ -0,0 +1,81 @@ +package org.schabi.newpipe.ui.components.items.stream + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.theme.AppTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StreamGridItem( + stream: Stream, + showProgress: Boolean, + isSelected: Boolean = false, + isMini: Boolean = false, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } + ) + .padding(12.dp) + ) { + val size = if (isMini) DpSize(150.dp, 85.dp) else DpSize(246.dp, 138.dp) + + StreamThumbnail( + stream = stream, + showProgress = showProgress, + modifier = Modifier.size(size) + ) + + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Text(text = stream.uploaderName, style = MaterialTheme.typography.bodySmall) + + Text( + text = stream.detailText, + style = MaterialTheme.typography.bodySmall + ) + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StreamGridItemPreview( + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream +) { + AppTheme { + Surface { + StreamGridItem(stream, showProgress = false, isSelected = false) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt new file mode 100644 index 00000000000..e2f698ade77 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt @@ -0,0 +1,13 @@ +package org.schabi.newpipe.ui.components.items.stream + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.ui.components.items.Stream + +internal class StreamItemPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + Stream(type = StreamType.NONE, name = "Stream", uploaderName = "Uploader"), + Stream(type = StreamType.LIVE_STREAM, name = "Stream", uploaderName = "Uploader"), + Stream(type = StreamType.AUDIO_LIVE_STREAM, name = "Stream", uploaderName = "Uploader"), + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt index 84fff3e74cf..b15fef2ee0a 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt @@ -20,17 +20,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.theme.AppTheme @OptIn(ExperimentalFoundationApi::class) @Composable fun StreamListItem( - stream: StreamInfoItem, + stream: Stream, showProgress: Boolean, isSelected: Boolean, - onClick: (StreamInfoItem) -> Unit = {}, - onLongClick: (StreamInfoItem) -> Unit = {}, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, onDismissPopup: () -> Unit = {} ) { // Box serves as an anchor for the dropdown menu @@ -58,10 +58,10 @@ fun StreamListItem( maxLines = 2 ) - Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall) + Text(text = stream.uploaderName, style = MaterialTheme.typography.bodySmall) Text( - text = getStreamInfoDetail(stream), + text = stream.detailText, style = MaterialTheme.typography.bodySmall ) } @@ -75,7 +75,7 @@ fun StreamListItem( @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun StreamListItemPreview( - @PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream ) { AppTheme { Surface { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt index 935bda85f99..8373db8436c 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt @@ -1,20 +1,18 @@ package org.schabi.newpipe.ui.components.items.stream import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel import org.schabi.newpipe.R import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.download.DownloadDialog -import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.dialog.PlaylistAppendDialog import org.schabi.newpipe.local.dialog.PlaylistDialog import org.schabi.newpipe.player.helper.PlayerHolder +import org.schabi.newpipe.ui.components.common.DropdownTextMenuItem +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.SparseItemUtil import org.schabi.newpipe.util.external_communication.ShareUtils @@ -22,59 +20,72 @@ import org.schabi.newpipe.viewmodels.StreamViewModel @Composable fun StreamMenu( - stream: StreamInfoItem, + stream: Stream, expanded: Boolean, onDismissRequest: () -> Unit ) { + val info = stream.toStreamInfoItem() val context = LocalContext.current - val streamViewModel = viewModel() val playerHolder = PlayerHolder.getInstance() DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { + val streamViewModel = viewModel() + if (playerHolder.isPlayQueueReady) { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.enqueue_stream)) }, + DropdownTextMenuItem( + text = R.string.enqueue_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueOnPlayer(context, it) } - } + }, ) if (playerHolder.queuePosition < playerHolder.queueSize - 1) { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.enqueue_next_stream)) }, + DropdownTextMenuItem( + text = R.string.enqueue_next_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueNextOnPlayer(context, it) } - } + }, ) } } - DropdownMenuItem( - text = { Text(text = stringResource(R.string.start_here_on_background)) }, + DropdownTextMenuItem( + text = R.string.start_here_on_background, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnBackgroundPlayer(context, it, true) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.start_here_on_popup)) }, + DropdownTextMenuItem( + text = R.string.start_here_on_popup, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnPopupPlayer(context, it, true) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.download)) }, + + if (stream.streamId != -1L) { + DropdownTextMenuItem( + text = R.string.delete, + onClick = { + onDismissRequest() + streamViewModel.deleteStreamHistory(stream.streamId) + }, + ) + } + + DropdownTextMenuItem( + text = R.string.download, onClick = { onDismissRequest() SparseItemUtil.fetchStreamInfoAndSaveToDatabase( @@ -85,52 +96,52 @@ fun StreamMenu( val fragmentManager = context.findFragmentActivity().supportFragmentManager downloadDialog.show(fragmentManager, "downloadDialog") } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.add_to_playlist)) }, + DropdownTextMenuItem( + text = R.string.add_to_playlist, onClick = { onDismissRequest() - val list = listOf(StreamEntity(stream)) + val list = listOf(StreamEntity(info)) PlaylistDialog.createCorrespondingDialog(context, list) { dialog -> val tag = if (dialog is PlaylistAppendDialog) "append" else "create" dialog.show( context.findFragmentActivity().supportFragmentManager, - "StreamDialogEntry@${tag}_playlist" + "StreamDialogEntry@${tag}_playlist", ) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.share)) }, + DropdownTextMenuItem( + text = R.string.share, onClick = { onDismissRequest() ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails) - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.open_in_browser)) }, + DropdownTextMenuItem( + text = R.string.open_in_browser, onClick = { onDismissRequest() ShareUtils.openUrlInBrowser(context, stream.url) - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.mark_as_watched)) }, + DropdownTextMenuItem( + text = R.string.mark_as_watched, onClick = { onDismissRequest() - streamViewModel.markAsWatched(stream) + streamViewModel.markAsWatched(info) } ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.show_channel_details)) }, + DropdownTextMenuItem( + text = R.string.show_channel_details, onClick = { onDismissRequest() SparseItemUtil.fetchUploaderUrlIfSparse( context, stream.serviceId, stream.url, stream.uploaderUrl ) { url -> val activity = context.findFragmentActivity() - NavigationHelper.openChannelFragment(activity, stream, url) + NavigationHelper.openChannelFragment(activity, info, url) } } ) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt index f5515a24a3c..28a5af6ae0d 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt @@ -1,77 +1,59 @@ package org.schabi.newpipe.ui.components.items.stream -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import coil3.compose.AsyncImage import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.components.items.common.Thumbnail import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.StreamTypeUtil -import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.viewmodels.StreamViewModel import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @Composable fun StreamThumbnail( - stream: StreamInfoItem, + stream: Stream, showProgress: Boolean, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit ) { Column(modifier = modifier) { - Box(contentAlignment = Alignment.BottomEnd) { - AsyncImage( - model = ImageStrategy.choosePreferredImage(stream.thumbnails), - contentDescription = null, - placeholder = painterResource(R.drawable.placeholder_thumbnail_video), - error = painterResource(R.drawable.placeholder_thumbnail_video), - contentScale = contentScale, - modifier = modifier - ) - - val isLive = StreamTypeUtil.isLiveStream(stream.streamType) - Text( - modifier = Modifier - .padding(2.dp) - .background(if (isLive) Color.Red else Color.Black.copy(alpha = 0.5f)) - .padding(2.dp), - text = if (isLive) { - stringResource(R.string.duration_live) - } else { - Localization.getDurationString(stream.duration) - }, - color = Color.White, - style = MaterialTheme.typography.bodySmall - ) - } + val isLive = StreamTypeUtil.isLiveStream(stream.type) + Thumbnail( + images = stream.thumbnails, + imageDescription = stringResource(R.string.stream_content_description, stream.name), + imagePlaceholder = R.drawable.placeholder_thumbnail_video, + cornerBackgroundColor = if (isLive) Color.Red else Color.Black.copy(alpha = 0.5f), + cornerIcon = null, + cornerText = if (isLive) { + stringResource(R.string.duration_live) + } else { + Localization.getDurationString(stream.duration) + }, + contentScale = contentScale, + modifier = modifier + ) if (showProgress) { val streamViewModel = viewModel() var progress by rememberSaveable { mutableLongStateOf(0L) } LaunchedEffect(stream) { - progress = streamViewModel.getStreamState(stream)?.progressMillis ?: 0L + progress = streamViewModel.getStreamState(stream.toStreamInfoItem())?.progressMillis ?: 0L } if (progress != 0L) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt deleted file mode 100644 index cdfe613edf3..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.schabi.newpipe.ui.components.items.stream - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import org.schabi.newpipe.extractor.Image -import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.extractor.stream.StreamType -import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.NO_SERVICE_ID -import java.util.concurrent.TimeUnit - -fun StreamInfoItem( - serviceId: Int = NO_SERVICE_ID, - url: String = "", - name: String = "Stream", - streamType: StreamType, - uploaderName: String? = "Uploader", - uploaderUrl: String? = null, - uploaderAvatars: List = emptyList(), - duration: Long = TimeUnit.HOURS.toSeconds(1), - viewCount: Long = 10, - textualUploadDate: String = "1 month ago" -) = StreamInfoItem(serviceId, url, name, streamType).apply { - this.uploaderName = uploaderName - this.uploaderUrl = uploaderUrl - this.uploaderAvatars = uploaderAvatars - this.duration = duration - this.viewCount = viewCount - this.textualUploadDate = textualUploadDate -} - -@Composable -internal fun getStreamInfoDetail(stream: StreamInfoItem): String { - val context = LocalContext.current - - return rememberSaveable(stream) { - val count = stream.viewCount - val views = if (count >= 0) { - when (stream.streamType) { - StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, count) - StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, count) - else -> Localization.shortViewCount(context, count) - } - } else { - "" - } - val date = - Localization.relativeTimeOrTextual(context, stream.uploadDate, stream.textualUploadDate) - - if (views.isEmpty()) { - date.orEmpty() - } else if (date.isNullOrEmpty()) { - views - } else { - "$views • $date" - } - } -} - -internal class StreamItemPreviewProvider : PreviewParameterProvider { - override val values = sequenceOf( - StreamInfoItem(streamType = StreamType.NONE), - StreamInfoItem(streamType = StreamType.LIVE_STREAM), - StreamInfoItem(streamType = StreamType.AUDIO_LIVE_STREAM), - ) -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt index 5d77488c5b7..eea317bbad4 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -21,65 +20,67 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.edit +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.flowOf import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ui.components.items.ItemList -import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem -import org.schabi.newpipe.ui.emptystate.EmptyStateComposable -import org.schabi.newpipe.ui.emptystate.EmptyStateSpec +import org.schabi.newpipe.ui.components.items.Playlist +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID +import java.util.concurrent.TimeUnit @Composable fun RelatedItems(info: StreamInfo) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) + val context = LocalContext.current + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val key = stringResource(R.string.auto_queue_key) // TODO: AndroidX DataStore might be a better option. var isAutoQueueEnabled by rememberSaveable { mutableStateOf(sharedPreferences.getBoolean(key, false)) } + val displayItems = info.relatedItems.mapNotNull { + when (it) { + is StreamInfoItem -> Stream(it) + is PlaylistInfoItem -> Playlist(it) + else -> null + } + } ItemList( - items = info.relatedItems, + items = flowOf(PagingData.from(displayItems)).collectAsLazyPagingItems(), mode = ItemViewMode.LIST, - listHeader = { - item { + header = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(R.string.auto_queue_description)) + Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 12.dp, end = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically ) { - Text(text = stringResource(R.string.auto_queue_description)) - - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = stringResource(R.string.auto_queue_toggle)) - Switch( - checked = isAutoQueueEnabled, - onCheckedChange = { - isAutoQueueEnabled = it - sharedPreferences.edit { - putBoolean(key, it) - } + Text(text = stringResource(R.string.auto_queue_toggle)) + Switch( + checked = isAutoQueueEnabled, + onCheckedChange = { + isAutoQueueEnabled = it + sharedPreferences.edit { + putBoolean(key, it) } - ) - } - } - } - if (info.relatedItems.isEmpty()) { - item { - EmptyStateComposable( - spec = EmptyStateSpec.NoVideos, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 128.dp) + } ) } } @@ -87,6 +88,26 @@ fun RelatedItems(info: StreamInfo) { ) } +private fun StreamInfoItem( + serviceId: Int = NO_SERVICE_ID, + url: String = "", + name: String = "Stream", + streamType: StreamType, + uploaderName: String? = "Uploader", + uploaderUrl: String? = null, + uploaderAvatars: List = emptyList(), + duration: Long = TimeUnit.HOURS.toSeconds(1), + viewCount: Long = 10, + textualUploadDate: String = "1 month ago" +) = StreamInfoItem(serviceId, url, name, streamType).apply { + this.uploaderName = uploaderName + this.uploaderUrl = uploaderUrl + this.uploaderAvatars = uploaderAvatars + this.duration = duration + this.viewCount = viewCount + this.textualUploadDate = textualUploadDate +} + @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt index 3e030407c92..fea1d507380 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt @@ -4,10 +4,6 @@ package org.schabi.newpipe.ui.emptystate import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -22,17 +18,12 @@ fun ComposeView.setEmptyStateComposable( setViewCompositionStrategy(strategy) setContent { AppTheme { - CompositionLocalProvider( - LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) - ) { - EmptyStateComposable( - spec = spec, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 128.dp) - - ) - } + EmptyStateComposable( + spec = spec, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 128.dp) + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt index 673a228928a..a2580a1c5c3 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt @@ -77,7 +77,7 @@ fun AboutScreen(padding: PaddingValues) { @Composable private fun AboutScreenPreview() { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { AboutScreen(PaddingValues(8.dp)) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt new file mode 100644 index 00000000000..55f44293811 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -0,0 +1,105 @@ +package org.schabi.newpipe.ui.screens + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import org.schabi.newpipe.R +import org.schabi.newpipe.local.history.HistoryViewModel +import org.schabi.newpipe.local.history.SortKey +import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.player.playqueue.SinglePlayQueue +import org.schabi.newpipe.ui.components.common.PlaybackControlButtons +import org.schabi.newpipe.ui.components.items.ItemList +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { + val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() + val historyItems = viewModel.historyItems.collectAsLazyPagingItems() + val streams = historyItems.itemSnapshotList.mapNotNull { it?.toStreamInfoItem() } + val queue = SinglePlayQueue(streams, 0) + + ItemList(historyItems, header = { + HistoryHeader( + sortKey = sortKey, + onSelectSortKey = viewModel::updateOrder, + queue = queue + ) + }) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun HistoryHeader( + sortKey: SortKey, + queue: PlayQueue, + onSelectSortKey: (SortKey) -> Unit, +) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + ) { + HistorySortRow(sortKey, onSelectSortKey) + + PlaybackControlButtons(queue) + } +} + +@Composable +private fun HistorySortRow( + sortKey: SortKey, + onSelectSortKey: (SortKey) -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(R.string.history_sort_label)) + + SingleChoiceSegmentedButtonRow { + SortKey.entries.forEachIndexed { index, key -> + SegmentedButton( + selected = key == sortKey, + onClick = { onSelectSortKey(key) }, + shape = SegmentedButtonDefaults + .itemShape(index = index, count = SortKey.entries.size) + ) { + Text(text = stringResource(key.title)) + } + } + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun HistoryHeaderPreview() { + AppTheme { + Surface { + HistoryHeader(SortKey.MOST_PLAYED, SinglePlayQueue(listOf(), 0)) {} + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt index d436b35a2e6..d475183e5ee 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt @@ -85,7 +85,10 @@ private val darkScheme = darkColorScheme( surfaceContainerHighest = surfaceContainerHighestDark, ) -private val blackScheme = darkScheme.copy(surface = Color.Black) +private val blackScheme = darkScheme.copy( + background = Color.Black, + surface = Color.Black +) @Composable fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index aba27c259ee..34c89b3f752 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -15,7 +15,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -52,7 +51,7 @@ import org.schabi.newpipe.ktx.ContextKt; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; -import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.history.HistoryFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; @@ -138,7 +137,7 @@ public static Intent getPlayerEnqueueNextIntent(@NonNull final Context conte } /* PLAY */ - public static void playOnMainPlayer(final AppCompatActivity activity, + public static void playOnMainPlayer(final FragmentActivity activity, @NonNull final PlayQueue playQueue) { final PlayQueueItem item = playQueue.getItem(); if (item != null) { @@ -562,7 +561,7 @@ public static void openLocalPlaylistFragment(final FragmentManager fragmentManag public static void openStatisticFragment(final FragmentManager fragmentManager) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, new StatisticsPlaylistFragment()) + .replace(R.id.fragment_holder, HistoryFragment.class, null, null) .addToBackStack(null) .commit(); } diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt index fff8d6b71fa..c601450a0c3 100644 --- a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt @@ -3,6 +3,7 @@ package org.schabi.newpipe.viewmodels import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import kotlinx.coroutines.rx3.awaitSingleOrNull @@ -23,4 +24,10 @@ class StreamViewModel(application: Application) : AndroidViewModel(application) historyRecordManager.markAsWatched(stream).await() } } + + fun deleteStreamHistory(streamId: Long) { + viewModelScope.launch(Dispatchers.IO) { + historyRecordManager.deleteStreamHistoryAndState(streamId).await() + } + } } diff --git a/app/src/main/res/layout/statistic_playlist_control.xml b/app/src/main/res/layout/statistic_playlist_control.xml index 36540d32e91..f3491078e23 100644 --- a/app/src/main/res/layout/statistic_playlist_control.xml +++ b/app/src/main/res/layout/statistic_playlist_control.xml @@ -30,7 +30,7 @@ android:layout_height="50dp" android:layout_toRightOf="@id/sortButtonIcon" android:gravity="left|center" - android:text="@string/title_most_played" + android:text="@string/history_sort_views" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="15sp" android:textStyle="bold" diff --git a/app/src/main/res/values-ar-rLY/strings.xml b/app/src/main/res/values-ar-rLY/strings.xml index bbf5b8bdf5f..c54aedf7519 100644 --- a/app/src/main/res/values-ar-rLY/strings.xml +++ b/app/src/main/res/values-ar-rLY/strings.xml @@ -406,7 +406,6 @@ اجراء الإيماءة اليمنى الرموز المسموح بها في أسماء الملفات %1$s %2$s - آخر ما تم تشغيله استخدم دائمًا الحل البديل لإعداد سطح إخراج فيديو ExoPlayer البث التالي تم تعطيل نفق وسائل الإعلام عن طريق التقصير على جهازك لأن نموذج جهازك معروف بأنه لا يدعمه. @@ -496,7 +495,6 @@ عرض نتائج ل: %s افتح باستخدام هل تريد حذف هذا العنصر من سجل البحث؟ - الأكثر تشغيلا عرض الوقت الأصلي على العناصر استعادة مِن الدقة الافتراضية diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b2c6c4b64e5..9ad1553ccee 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -286,8 +286,6 @@ لا يوجد بث متاح للتنزيل تم حذف عنصر واحد. NewPipe هو برنامج مفتوح المصدر وبحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. وعلى وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار أحدث. - آخر ما تم تشغيله - الأكثر تشغيلا هذا سوف يُزيل إعداداتك الحالية. طريقة \'التشغيل\' المفضلة الإجراء الافتراضي عند فتح المحتوى — %s @@ -572,7 +570,6 @@ عرض نتائج ل: %s أبدا فقط على شبكة Wi-Fi - بدء التشغيل تلقائياً — %s تشغيل قائمة الانتظار تعذر التعرف على الرابط. فتح باستخدام تطبيق آخر؟ قائمة انتظار تلقائيّة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 3055db9e60a..5abd5f9680e 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -335,8 +335,6 @@ NewPipe Lisenziyası Tarixçə Bu elementi axtarış tarixçəsindən silmək istəyirsiniz\? - Son Oynadılan - Ən Çox Oynadılan Bölmə seç İdxal edildi Etibarlı ZIP faylı yoxdur diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml index 454ddd15229..f3cf5e5f054 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -412,8 +412,6 @@ Páxina d\'una canal Quioscu predetermináu Páxina de quioscu - Lo más reproducío - Lo último reproducío NewPipe ye software copyleft: pues usalu, estudialu, compartilu y ameyoralu como quieras. N\'especial, pues redistribuyilu y/o modificalu baxo los términos de la GNU General Public License según espublizó la Free Software Foundation, quier la versión 3 de la llicencia quier (na to opinión) cualesquier versión posterior. El proyeutu de NewPipe toma mui en serio la privacidá. Poro, l\'aplicación nun recueye nengún datu ensin el to consentimientu. \nLa política de privacidá de NewPipe desplica en detalle los datos que s\'unvien y atroxen cuando unvies un informe de casque. diff --git a/app/src/main/res/values-b+uz+Latn/strings.xml b/app/src/main/res/values-b+uz+Latn/strings.xml index 780061c73a8..a4feaeccc3c 100644 --- a/app/src/main/res/values-b+uz+Latn/strings.xml +++ b/app/src/main/res/values-b+uz+Latn/strings.xml @@ -182,8 +182,6 @@ Bo\'sh sahifa Asosiy sahifada qanday yorliqlar ko\'rsatilgan Asosiy sahifaning tarkibi - Eng ko\'p ijrolar etilganlar - Oxirgi ijro Ushbu narsani qidiruv tarixidan o\'chirmoqchimisiz\? Tarix Tarix diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 19f3c8b2628..1febff0d124 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -232,9 +232,7 @@ Гісторыя Гісторыя Выдаліць гэты элемент з гісторыі пошуку? - Прайгравалася нядаўна - Прайгравалася найбольш - Змесціва галоўнай старонкі + Кантэнт галоўнай старонкі Пустая старонка Старонка кіёска Старонка канала diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 9264986492e..64e4506a68c 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -224,8 +224,6 @@ История История Искате ли да изтриете този елемент от историята на търсенията? - Последно възпроизвеждани - Най-възпроизвеждани Съдържание на главната страница Празна страница Страница-павилион diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index ddc32e4187c..4c8ee27f15e 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -195,8 +195,6 @@ একটি চ্যানেল পছন্দ করুন চ্যানেল এর পাতা খালি পাতা - সবথেকে বেশি চালানো - শেষ চালানো লাইসেন্স পড়ুন নিউপাইপ এর লাইসেন্স প্রাইভেসি পলিসি পড়ুন diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index e6269b5b931..ad3d7cd4d49 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -233,8 +233,6 @@ অডিও সেটিং বিবরণ স্থানীয় - সবথেকে বেশি চালানো - শেষ চালানো ফিরিয়ে দিন যোগদান নিউ পাইপ এর সম্বন্ধে diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 14201bc9e87..ea7f0aebf04 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -59,8 +59,6 @@ একটি চ্যানেল পছন্দ করুন চ্যানেল এর পাতা খালি পাতা - সবথেকে বেশি চালানো - শেষ চালানো ইতিহাস ইতিহাস লাইসেন্স পড়ুন diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 1a9463d05bf..a117e54aa1b 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -194,8 +194,6 @@ Visualitza a GitHub Feu una donació Per a més informació i notícies, visiteu el nostre web. - Últimes reproduccions - Més reproduïts Tendències Pàgina d\'un canal Trieu un quiosc diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 5d6c1d9d94f..2ce942a833a 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -101,7 +101,6 @@ مۆڵەتنامەی نیوپایپ پیشاندانی ڕێنمایی ”داگرتن تا پاشکۆ” دابەشکراوەکان - زۆرترین لێدراو لادانی نیشانه‌كراو مۆڵەتەکان ناتوانرێت به‌ژداریكردنه‌كه‌ نوێبكرێته‌وه‌ @@ -312,7 +311,6 @@ مێژوو دەسڕێتەوە لەگەڵ په‌خشه‌ لێدراوه‌كان و شوێنی کارپێکەر دانان-خۆکار ١ بابەت سڕایەوە. - کرداری بنەڕەتی لەکاتی کردنەوەی بابەتدا — %s هکیۆسکێک دیار بکە کۆنفرانسەکان كردنه‌وه‌ له‌ دۆخی په‌نجه‌ره‌ @@ -507,7 +505,6 @@ ڕێکخستنەکانی دەنگ پرست پێ دەکرێت بۆ شوێنی دابەزاندنی هەر بابەتێک. \nهەڵبژێرەری فۆڵدەری سیستەم کارابکە (SAF) گەر دەتەوێت بابەتەکانت لە بیرگەی دەرەکیدا داببەزێنرێن - دواین لێدراو ناتوانرێ لیستی دابه‌زاندن دابنرێت وەشانی نوێی نیوپایپ بەردەستە! وێنۆچکەی خشتەلێدان گۆڕدرا. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index bb0a1ec7bf8..3b188914eaa 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -225,8 +225,6 @@ Vytvořit Zahodit Přejmenovat - Poslední přehráno - Nejvíce přehráno Vždy se zeptat Nový playlist Přejmenovat diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 0312b3c74b4..1fb78773439 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -228,8 +228,6 @@ Historik Historik Vil du slette dette element fra søgehistorikken\? - Sidst Afspillet - Mest Afspillet Indhold af hovedside Hvilke faner vises på hovedsiden Tom Side diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e782e700aa2..264d6b87664 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -220,8 +220,6 @@ Zum Neuordnen ziehen Erstellen Umbenennen - Zuletzt wiedergegeben - Am häufigsten wiedergegeben Immer fragen Neue Wiedergabeliste Umbenennen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index c0b09ad1335..0d26aea986f 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -228,8 +228,6 @@ Το NewPipe είναι copylelft ελεύθερο λογισμικό: Μπορείτε να το χρησιμοποιήσετε, να το μελετήσετε, να το μοιραστείτε και να το βελτιώσετε κατά βούληση. Ειδικότερα, μπορείτε να το αναδιανείμετε ή/και να το τροποποιήσετε υπό την άδεια GNU General Public Licence όπως αυτή εκδόθηκε από το Free Software Foundation, είτε υπό την έκδοση 3 της άδειας, είτε (προαιρετικά) υπό οποιαδήποτε μεταγενέστερη άδεια. Ανάγνωση της άδειας Θέλετε να σβήσετε αυτό το αντικείμενο από το ιστορικό αναζήτησης; - Τελευταία αναπαραγωγή - Αναπαράχθηκε περισσότερο Περιεχόμενο της κεντρικής σελίδας Κενή σελίδα Σελίδα περιπτέρου diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 66e2c4d104f..7244e8fb1b6 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -318,8 +318,6 @@ Krei Rezigni Alinomi - Lasta Ludado - Plej ludataj filmetoj Neniuj Subtitoloj Alĝustigi Plenigi diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c1efd2ee759..91bb78a349b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -225,8 +225,6 @@ Crear Descartar Cambiar nombre - Última reproducción - Más reproducido Preguntar siempre Lista de reproducción nueva Cambiar nombre diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index a89d2846b66..86e9f6b468c 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -213,8 +213,6 @@ Ajalugu Ajalugu Kas kustutame selle kirje otsinguajaloost\? - Viimati esitatud - Enim esitatud Avalehe sisu Tühi leht Kioski leht diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 415d2a8d348..257bfb7c4af 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -241,8 +241,6 @@ Baztertu Aldatu izena Elementu 1 ezabatuta. - Jotako azkena - Ikusiena Esportatuta Inportatuta Ez da baliozko ZIP fitxategia diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 6a252e22f9a..61df1fe2d3e 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -189,8 +189,6 @@ تاریخچه تاریخچه می‌خواهید این مورد را از تاریخچه جستجو پاک کنید؟ - آخرین پخش‌شده - بیشترین پخش‌شده محتوای صفحه اصلی صفحه خالی صفحه کیوسک diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 300493b520b..06460751ca0 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -244,8 +244,6 @@ Lisää ehdotettu suoratoistosisältö automaattisesti soittolistaan Suoratoistosisältöä ei saatavilla ladattavaksi NewPipe on vapaata ohjelmistoa. Voit käyttää, opiskella, jakaa ja parantaa sitä mielesi mukaan. Tarkemmin sanottuna voit jakaa sitä edelleen ja/tai muokata sitä Free Software Foundationin julkaiseman GNU General Public Licensen, version 3 tai uudemman, ehdoilla. - Viimeksi toistettu - Eniten toistetut Vienti valmis Tuonti valmis Virheellinen ZIP-tiedosto diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 5dfcbd2ce18..1716df2dc13 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -170,7 +170,6 @@ Mga Kabanata Walang app sa device mo ang makakabukas nito Tampok - Huling Pinanood Kasaysayan Walang nakikinig Walang nahanap @@ -181,7 +180,6 @@ Mga %s nanonood Walang mga video - Madalas na Pinanood %s video Mga %s na video diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 48710619b66..0a2c62943dc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -224,8 +224,6 @@ Créer Rejeter Renommer - Dernière lecture - Vidéos les plus vues Toujours demander Nouvelle liste de lecture Renommer diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 586e351e24a..20f86061b91 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -227,8 +227,6 @@ Historial Historial Desexa eliminar este elemento do historial de procura? - Última reprodución - Máis reproducido Contido da páxina principal Páxina en branco Páxina do «kiosk» diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index b4c16a47364..4bda7a27a9f 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -291,8 +291,6 @@ \nמדיניות הפרטיות של NewPipe מסבירה בפרטי פרטים אילו נתונים נשלחים ומאוחסנים בעת שליחת דיווח על תקלה. הצגת מדיניות הפרטיות NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. - התנגנו אחרונים - הכי נצפים אזהרה: ייבוא חלק מהקבצים נכשל. פעולה זו תדרוס את ההגדרות הקיימות. לייבא גם הגדרות\? diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 890ef13477e..82f603c35fc 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -210,8 +210,6 @@ वापस दें वेबसाइट अधिक जानकारी और खबरों के लिए न्यूपाइप की वेबसाइट पर जाएं। - पिछला चलाया गया - अधिकतम चलाए गए निर्यात संपन्न हुआ आयात संपन्न हुआ कोई वैध ज़िप फ़ाइल नहीं है diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 960106e0dcc..ad46a8e59d4 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -252,8 +252,6 @@ Posjeti NewPipe web-stranicu za više informacija i vijesti. NewPipe pravila o privatnosti Pročitaj pravila o privatnosti - Zadnje svirano - Najviše reproducirano Izvezeno Uvezeno Nema važeće ZIP datoteke diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index fb04aa724b8..d9731488447 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -218,8 +218,6 @@ Előzmények Előzmények Törli ezt az elemet a keresési előzmények közül\? - Utoljára lejátszott - Legtöbbet lejátszott Főoldal tartalma Üres oldal Kioszk oldal diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index d0bc29057bd..bcf1ac8b5d1 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -136,8 +136,6 @@ Licentia de NewPipe Leger le licentia Chronologia - Ultime reproductiones - Le plus reproducite Contento del pagina principal Selige un canal Preste diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 4117e3a18e0..56bf485d142 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -190,8 +190,6 @@ Situs Web Kunjungi situs web NewPipe untuk melihat info dan berita lebih lanjut. Apakah Anda ingin menghapus item ini dari riwayat pencarian\? - Terakhir Diputar - Sering Diputar Konten halaman utama Halaman Kosong Halaman Kedai diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index c5d5d02c615..9485f42baf3 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -265,8 +265,6 @@ Ferill Ferill Lesa leyfi - Nýlega spilað - Mest spilað Aðalsíða Tungumálið breytist þegar forritið er endurræst Flutt út diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6767f857239..753a36182d4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -225,8 +225,6 @@ Crea Ignora Rinomina - Ultima riproduzione - I più riprodotti Chiedi ogni volta Nuova playlist Rinomina diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 64e415559b7..0a72ebb6264 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -268,8 +268,6 @@ NewPipe プロジェクトはあなたのプライバシーを非常に大切にしています。あなたの同意がない限り、アプリはいかなるデータも収集しません。 \nNewPipe のプライバシー・ポリシーでは、クラッシュリポート送信時にどのような種類のデータが送信・記録されるかを詳細に説明しています。 NewPipe はコピーレフトなソフトウェアです。あなたは自由にそれを使用し、研究し、共有し、そして改善することができます。あなたは、GNU フリーソフトウェア財団が公開する GNU General Public ライセンス バージョン3以降の下に、自由に再配布・修正を行うことができます。 - 最終再生日時 - 最も再生された動画 拡大 プレイリスト 「長押しでキューに追加」のヒントを表示 diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index fd0b3f2ab6d..00f6d10c9b2 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -317,8 +317,6 @@ ისტორია ისტორია გსურთ წაშალოთ ეს ელემენტი ძიების ისტორიიდან\? - ბოლოს დაუკრა - ხშირად დაკრული მთავარი გვერდის შინაარსი რა ჩანართებია ნაჩვენები მთავარ გვერდზე გადაფურცლეთ ელემენტები მათი ამოსაშლელად diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index eccaaccb9cb..5cab4276997 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -92,7 +92,6 @@ Ulac Aḍris yettwanγel γef afus Tibdarin n tɣuri - Aneggaru yettwaslekmen Taɣuri tawurmant Aneqqis Sider @@ -214,7 +213,6 @@ Ugar n tnefrunin Bḍu d Talqayt : - Tid yettwaɣran s waṭas Fren iccer Tutlayt [Arussin] diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml index 9f19ced10a7..755d03c10f4 100644 --- a/app/src/main/res/values-kmr/strings.xml +++ b/app/src/main/res/values-kmr/strings.xml @@ -158,8 +158,6 @@ Rûpelê Vala Kîjan tabî di rûpelê sereke de têne nîşandin Naveroka rûpelê sereke - Pir lîstin - Lîstika dawîn Ma hûn dixwazin vî tiştî ji dîroka lêgerînê paqij bikin\? Dîrok Dîrok diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0bd08a5e430..50557808d06 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -209,8 +209,6 @@ 취소 이름 바꾸기 reCAPTCHA 확인 - 마지막으로 재생 - 가장 많이 재생 내보내기 완료 가져오기 완료 유효한 ZIP 파일 없음 diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 4b4c6f2c968..34efc046ad2 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -190,8 +190,6 @@ مێژوو مێژوو ئایا دەتەوێ ئەم بابەتە لە مێژووی گەڕان بسڕدرێتەوە؟ - دواین کارپێکراو - زۆرترین کارپێکراو ناوەڕۆکی پەڕەی سەرەکی لادان وردەکارییەکان diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 40fe8e8adba..da372df2a79 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -214,8 +214,6 @@ Kurti Nutraukti Pervardyti - Vėliausiai žiūrėta - Dažniausiai žiūrėta Eksportavimas baigtas Importavimas baigtas Netinkamas ZIP failas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 9d99c14d6c6..846155c775a 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -46,8 +46,6 @@ Tukša Lapa Kuras cilnes rāda galvenajā lapā Galvenās lapas saturs - Visvairāk Atskaņotais - Pēdējais Atskaņotais Vai jūs vēlaties izdzēst šo lietu no meklēšanas vēstures\? Vēsture Vēsture diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index ba41b730bb7..6312ed0e218 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -209,8 +209,6 @@ Историја Историја Сакаш да го избришеш предметот од историјата? - Последно пуштено - Најгледани Содржина Празна страна Киоск diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index c6191ca7bd6..8db75591bdb 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -98,8 +98,6 @@ ശൂന്യമായ പേജ് പ്രധാന പേജിൽ കാണിക്കേണ്ട ടാബുകൾ പ്രധാന പേജ് ഉള്ളടക്കം - ഏറ്റവും കൂടുതൽ തവണ പ്ലേ ചെയ്തത് - അവസാനം പ്ലേ ചെയ്തത് സെർച്ച് ചരിത്രത്തിൽനിന്ന് ഈ item നീക്കം ചെയ്യട്ടെയോ\? ചരിത്രം ചരിത്രം diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index bb0527655ea..d17c54a3b56 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -235,8 +235,6 @@ Sejarah Sejarah Adakah anda mahu memadamkan item ini dari sejarah carian\? - Terakhir dimainkan - Kebanyakan dimainkan Kandungan halaman utama Tab apa yang ditunjukkan pada halaman utama Halaman Kosong diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 9e3916607a4..40ff47df87e 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -222,8 +222,6 @@ Opprett Forkast Gi nytt navn - Sist spilt - Mest spilt Alltid spør Ny spilleliste Gi nytt navn diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 679b10846e5..855b9185dd4 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -242,8 +242,6 @@ इतिहास इतिहास तपाईं खोज इतिहासबाट यो वस्तु मेटाउन चाहनुहुन्छ\? - पछिल्लो पालि खोलिएको - धेरै हेरिएको मुख्य पृष्ठको सामग्री मुख्य पृष्ठ मा कुनकुन ट्याबहरू देखाइन्छ खाली पृष्ठ diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index d96dc06943f..ae12475015e 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -210,8 +210,6 @@ Geschiedenis Geschiedenis Wilt u dit item verwijderen uit uw zoekgeschiedenis\? - Laatst afgespeeld - Meest afgespeeld Inhoud van hoofdpagina Blanco pagina Kioskpagina diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 00f095e39f2..67560602b09 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -222,8 +222,6 @@ Aanmaken Sluiten Naam wijzigen - Laatst afgespeeld - Meest afgespeeld Altijd vragen Nieuwe afspeellijst Naam wijzigen diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index e0b812410f0..185c2b6b918 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -321,7 +321,6 @@ ߣߴߌ ߞߊ߬ ߜߟߍ߬ߦߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߟߥߊߟߌߟߊ߲ ߠߊߓߊ߯ߙߊ ߘߐ߫߸ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ߬ ߞߍߣߍ߲߫ ߛߌߦߊߡߊ߲ ߠߎ߬ ߡߊߝߍߣߍ߲߫ ߹ ߘߝߐ߬ߦߊ ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߝߌ߬ߛߌ ߣߌ߲߬ ߖߐ߬ߛߌ߫ ߢߌߣߌ߲ߠߌ߲߫ ߘߝߐ߬ߦߊ ߟߎ߬ ߘߐ߫؟ - ߦߋߡߍ߲ߕߊ߫ ߦߋߣߍ߲ߓߊ ߟߎ߬ ߓߏ߬ߟߏ߲߬ ߞߐߜߍ ߞߣߐߘߐ ߏ߬ ߘߴߌ ߟߊ߫ ߛߋ߲߬ߠߊ߬ ߢߊߓߐߟߌ ߟߎ߬ ߓߍ߯ ߝߌߘߊ߲߫. ߛߎߥߊ߲ߘߟߌ ߞߍ߫ ߛߏ߬ߙߌ߲߬ߘߐ ߟߎ߬ ߟߊ߫߸ ߡߍ߲ ߠߎ߫ ߦߌ߬ߘߊ߬ߕߐ߫ ߓߏ߬ߟߏ߲߬ ߞߐߜߍ ߞߊ߲߬ @@ -396,7 +395,6 @@ ߞߊ߬ ߟߊ߬ߘߌߢߍ ߞߊ߬ߙߊ߲߬ ߊ߬ ߡߊߝߍߣߍ߲߫ ߗߍߦߙߐ ߟߊ߫ ߘߝߐ߬ߦߊ - ߞߐߟߕߊ߫ ߕߏߟߏ߲ߣߍ߲ ߠߎ߬ ߥߙߏߝߋ߫ ߞߐߜߍ ߝߎ߲ߞߎ߲ߟߋ߲ ߥߙߏߝߋ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫ diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 92e6cff376a..13519e3c614 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -320,7 +320,6 @@ ଗୋପନୀୟତା ନୀତି ପଢ଼ନ୍ତୁ NewPipe ର ଲାଇସେନ୍ସ ଲାଇସେନ୍ସ ପଢ଼ନ୍ତୁ - ଶେଷ ଥର ପ୍ଲେ ହୋଇଛି ଖାଲି ପୃଷ୍ଠା ଚ୍ୟାନେଲ୍ ପୃଷ୍ଠା କିଓସ୍କ ପୃଷ୍ଠା @@ -628,7 +627,6 @@ ଯଦି ଆପଣ ଆପ୍ ବ୍ୟବହାର କରିବାରେ ଅସୁବିଧାର ସମ୍ମୁଖୀନ ହେଉଛନ୍ତି, ସାଧାରଣ ପ୍ରଶ୍ନର ଏହି ଉତ୍ତରଗୁଡିକ ଯାଞ୍ଚ କରିବାକୁ ନିଶ୍ଚିତ ହୁଅନ୍ତୁ! ୱେବସାଇଟ୍ ରେ ଦେଖନ୍ତୁ ଆପଣ ସନ୍ଧାନ ଇତିହାସରୁ ଏହି ଆଇଟମ୍ ବିଲୋପ କରିବାକୁ ଚାହୁଁଛନ୍ତି କି\? - ଅଧିକାଂଶ ପ୍ଲେ ହୋଇଛି ମୁଖ୍ୟ ପୃଷ୍ଠାରେ କେଉଁ ଟ୍ୟାବଗୁଡ଼ିକ ଦେଖାଯାଏ ଏକ ଚ୍ୟାନେଲ୍ ଚୟନ କରନ୍ତୁ ସେଗୁଡିକ ଅପସାରଣ କରିବା ପାଇଁ ଆଇଟମଗୁଡିକ ସ୍ୱାଇପ୍ କରନ୍ତୁ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 3d397525194..e618ace0fb3 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -218,8 +218,6 @@ ਇਤਿਹਾਸ ਇਤਿਹਾਸ ਕੀ ਤੁਸੀਂ ਇਸਨੂੰ ਖੋਜ ਇਤਿਹਾਸ ਵਿੱਚੋਂ ਮਿਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ\? - ਆਖਰੀ ਚਲਾਈ ਗਈ - ਸਭ ਤੋਂ ਜਿਆਦਾ ਚਲਾਈ ਗਈ ਮੁੱਖ ਪੰਨੇ ਦੀ ਸਮੱਗਰੀ ਖ਼ਾਲੀ ਪੰਨਾ ਕਿਓਸਕ ਪੰਨਾ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e260cc8878c..1f7d130559a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -221,8 +221,6 @@ Utwórz Odrzuć Zmień nazwę - Ostatnio odtwarzane - Najczęściej odtwarzane Wyeksportowano Zaimportowano Nieprawidłowy plik ZIP diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e88e27a1892..75ed98900e1 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -223,8 +223,6 @@ Criar Descartar Renomear - Última reprodução - Mais assistidos Sempre perguntar Nova playlist Renomear diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 15c174cd844..f557028a63c 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -265,7 +265,6 @@ \'Storage Access Framework\' permite transferências para um cartão SD externo Duração da pesquisa de avanço/recuo rápido Ficheiro movido ou eliminado - Última reprodução Visite o site NewPipe para obter mais informação e novidades. Importe o seu perfil SoundCloud digitando o URL ou a ID.: \n @@ -316,7 +315,6 @@ Fonte Página da lista de reprodução Definições - Mais reproduzido A mostrar resultados para: %s Mudar para segundo plano Álbuns diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6d53e9fd916..7279d64c617 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -230,8 +230,6 @@ Limpar todos os dados da página web Meta-dados em cache limpos Ficheiro - Última reprodução - Mais reproduzido Reprodutor de vídeo Reprodutor em segundo plano Reprodutor \'popup\' diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 5b1238b0c11..b3fc0f40ddb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -217,8 +217,6 @@ Dăruiește înapoi Site web Vizitați site-ul NewPipe pentru mai multe informații și noutăți. - Ultimele vizionări - Cele mai multe vizionări Exportat Importat Nici un fișier ZIP valid diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a212d9888c0..7efe16b3df1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -242,8 +242,6 @@ Создать Скрыть Переименовать - Недавно проигранные - Часто проигрываемые Экспорт завершён Импорт завершён Нет верного Zip-файла diff --git a/app/src/main/res/values-ryu/strings.xml b/app/src/main/res/values-ryu/strings.xml index 1a1383015d7..91538820bb9 100644 --- a/app/src/main/res/values-ryu/strings.xml +++ b/app/src/main/res/values-ryu/strings.xml @@ -270,8 +270,6 @@ NewPipeプロジェクトーうんじゅがプライバシーふぃじょうにてーしちなちょーいびーん。うんじゅがちゃーいがねーんかぎり、アプレーいかなるデータんしゅうしゅうさびらん。 \nNewPipeぬプライバシー・ポリシーっしぇー、クラッシュリポートそうしんじんかいちゃぬぐとーるしゅるいぬデータぬあんしん・きるくされいがしーょうさいにしちめいそーいびーん。 NewPipeーコピーレフトなソフトウェアやいびーん。うんじょーじゆうにうりさし、きんきゅうしー、きょうゆうし、あんしがいじんするくとぅがなやびーん。うんじょー、GNUフリーソフトウェアじぇーやんんがかんかいすん GNU General Publicライセンスバージョン3いかぬむとぅんかい、じゆうにさえーいーん・しゅうせいうくないるくとぅがなやびーん。 - さいしゅうさいせいにちじ - むっとぅむさいせいさったんちゃーしが かくだい プレイリスト 「ながうしっしキューんかいちちが」ぬヒントひょうじ @@ -281,7 +279,6 @@ ながうしっしキューんかいちちが ポップアップっしりんずくささるゆいかいし うくぬみぬ「ふぃらく」アクション - コンテンツふぃらちゅるとぅちぬデフォルトちゃーさ — %s フィット じんぬみん じどうせいせい @@ -530,7 +527,6 @@ せいけいじみリポートコピー プレイリストページ プレイリストさんたくちくぃみそーれー - じちゃーてぃきなさうぅいゆいかいしさびーん — %s じちゃーっしキューんかいちちが アクティブやるプレイヤーぬキューぬいりちがーやびーん プレイヤーびちぬプレイヤーんかいきりけーいねーキューぬうきかわいるかのうゆいがあいびーん diff --git a/app/src/main/res/values-sat/strings.xml b/app/src/main/res/values-sat/strings.xml index c1a5aaccaca..2c21b7cba3c 100644 --- a/app/src/main/res/values-sat/strings.xml +++ b/app/src/main/res/values-sat/strings.xml @@ -506,8 +506,6 @@ ᱱᱟᱜᱟᱢ ᱱᱟᱜᱟᱢ ᱟᱢ ᱱᱚᱶᱟ ᱡᱤᱱᱤᱥ ᱥᱟᱸᱪᱟᱨ ᱱᱟᱜᱟᱢ ᱠᱷᱚᱱ ᱵᱚᱫᱚᱞ ᱢᱮᱢᱮ? - ᱢᱩᱪᱟᱹᱫ ᱠᱷᱮᱞ ᱟᱠᱟᱱᱟ - ᱡᱟᱹᱥᱛᱤ ᱠᱷᱮᱞ ᱟᱠᱟᱱ ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ ᱨᱮᱱᱟᱜ ᱥᱟᱦᱴᱟ ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ ᱨᱮ ᱚᱠᱟ ᱛᱟᱵᱽ ᱠᱚ ᱵᱚᱫᱚᱞ ᱟᱠᱟᱱᱟ ᱡᱤᱱᱤᱥ ᱠᱚ ᱵᱟᱧᱪᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱥᱣᱟᱭᱯ ᱢᱮ diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index fc3558ea300..145c87a6910 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -83,8 +83,6 @@ Pàgina bòida Ischedas benint ammustradas in sa pàgina printzipale Cuntenutu de sa pàgina printzipale - Prus riproduidos - Ùrtima riprodutzione Cheres iscantzellare custu elementu dae sa cronologia de chirca\? Cronologia Cronologia diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 88f1b13f5dd..95c664a2149 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -197,8 +197,6 @@ Webstránka Pre viac informácií a noviniek navštívte webstránku NewPipe. Chcete odstrániť túto položku z histórie vyhľadávania? - Naposledy prehrávané - Najprehrávanejšie Obsah na hlavnej stránke Prázdna strana Kiosk diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b0fcec406c9..9b30a4a82da 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -259,8 +259,6 @@ Uvoženo Izvoženo Ni še nobene naročnine - Najbolj igrano - Nazadnje igrano Preberi pravilnik zasebnosti NewPipe-ovi pravilnik zasebnosti Obiščite spletno mesto od NewPipe za več informacij in novic. diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index 7577cfba95b..3fc3008755f 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -158,8 +158,6 @@ Bog Madhan Daaqadaha lasoobandhigo bogga guud Bogga guud - Badanaa La Daawado - U Dambeeyay ee La Daawaday Ma rabtaa inaad ka saarto shaygan kaydka wixii la raadiyay\? Wixii Hore Akhri laysinka diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 873234e2396..2d35b941a33 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -224,8 +224,6 @@ Faqe Bosh Cilat tab-e shfaqen në faqen kryesore Përmbajtja e faqes kryesore - Më të Luajturat - Luajtur së Fundmi Doni ta fshini këtë objekt nga historiku i kërkimeve\? Historiku Historiku diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index e212a480b63..540abff22e0 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -380,8 +380,6 @@ Изаберите листу пуштања Подразумевани киоск Које картице се приказују на главној страници - Највише пуштано - Последње пуштано NewPipe је слободан софтвер за копирање: Можете га користити, проучавати, делити и побољшавати по жељи. Конкретно, можете га поново дистрибуирати и/или модификовати под условима GNU Опште јавне лиценце коју је објавила Фондација за слободни софтвер, било верзију 3 лиценце или (по вашем избору) било коју каснију верзију. Прочитај политику приватности Пројекат NewPipe веома озбиљно схвата вашу приватност. Стога, апликација не прикупља никакве податке без вашег пристанка. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index de74c27f7e6..362f6a5a59d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -244,8 +244,6 @@ \nNewPipes sekretesspolicy förklarar i detalj vad för data som skickas och lagras när du skickar en kraschrapport. Läs sekretesspolicy NewPipe är copyleft fri programvara: Du kan använda, studera, dela och förbättra den som du vill. Specifikt kan du distribuera och/eller modifiera det under villkoren för GNU General Public License som publicerats av Free Software Foundation, antingen version 3 av licensen, eller (om du så önskar) en senare version. - Senast spelade - Mest spelade Exporterad Importerad Ogiltig ZIP-fil diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 0056f74166c..63215efab81 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -605,8 +605,6 @@ நியூபைப் என்பது நகலெடுக்கப்பட்ட லிப்ரே மென்பொருள்: நீங்கள் அதைப் பயன்படுத்தலாம், படிக்கலாம், பகிரலாம் மற்றும் மேம்படுத்தலாம். குறிப்பாக நீங்கள் இலவச மென்பொருள் அறக்கட்டளையால் வெளியிடப்பட்ட குனு பொது பொது உரிமத்தின் விதிமுறைகளின் கீழ் மறுபகிர்வு மற்றும்/அல்லது மாற்றியமைக்கலாம், உரிமத்தின் பதிப்பு 3 அல்லது (உங்கள் விருப்பத்தில்) பின்னர் எந்த பதிப்பையும் மாற்றலாம். பயன்பாட்டைப் பயன்படுத்துவதில் சிக்கல் இருந்தால், பொதுவான கேள்விகளுக்கு இந்த பதில்களைப் பார்க்கவும்! இணையதளத்தில் காண்க - கடைசியாக விளையாடியது - அதிகம் விளையாடியது என்ன தாவல்கள் முதன்மையான பக்கத்தில் காட்டப்பட்டுள்ளன வெற்று பக்கம் கியோச்க் பக்கம் diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 538ace593f7..6bc0717d6de 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -331,7 +331,6 @@ దీనికి ఈ అనుమతి అవసరం \nతేలియాడే పద్ధతిలో తెరవండి © %3$s కింద %2$s ద్వారా %1$s - చివరిగా ఆడింది చరిత్ర, సభ్యత్వాలు, ప్లేజాబితాలు మరియు అమరికలను ఎగుమతిచేయుము మీ ప్రస్తుత చరిత్ర, సభ్యత్వాలు, ప్లేజాబితాలు మరియు (ఐచ్ఛికంగా) సెట్టింగ్‌లను భర్తీ చేస్తుంది డాటాబేసుని దిగుమతిచేయుము @@ -352,7 +351,6 @@ తిరిగి ఇవ్వండి వెబ్సైట్ NewPipe యొక్క గోప్యతా విధానం - ఎక్కువగా ఆడినవి ప్రధాన పేజీలో ఏ ట్యాబ్‌లు చూపబడతాయి కియోస్క్ పేజీ డిఫాల్ట్ కియోస్క్ diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index bcbbca0a42d..d889b40f383 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -222,8 +222,6 @@ ประวัติ ประวัติ คุณต้องการลบรายการนี้ออกจากประวัติการค้นหาหรือไม่\? - เล่นครั้งล่าสุด - เล่นมากที่สุด เนื้อหาของหน้าหลัก แท็บใดบ้างที่ต้องการให้แสดงบนหน้าหลัก หน้าว่าง diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1c736bb9907..40f307b03c4 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -218,8 +218,6 @@ Oluştur Dışla Yeniden adlandır - Son Oynatılan - En Çok Oynatılan Her zaman sor Yeni Oynatma Listesi Yeniden adlandır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c9fa9f25c75..415b61a4587 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -214,8 +214,6 @@ Прочитати ліцензію Історія Видалити цей елемент з історії пошуку\? - Відтворювалося останнім - Відтворювалося найбільше Вміст на головній сторінці Порожня сторінка Кіоск-сторінка diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 3bfa5deff31..28f2ec5043d 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -207,8 +207,6 @@ سرگزشت سرگزشت کیا آپ اس آئٹم کو تلاش کی سرگزشت سے حذف کرنا چاہتے ہیں؟ - آخری چلائی گئی - سب سے زیادہ چلائی گئی مرکزی صفحہ کا مواد خالی صفحہ رجحان صفحہ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 179f566e885..b14e51ed3f6 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -225,8 +225,6 @@ Lịch sử Lịch sử Bạn có muốn xóa mục này khỏi lịch sử tìm kiếm không? - Lần phát cuối - Được phát nhiều nhất Nội dung trang chính Trang trống Trang chủ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7f9576d7fd2..d538c81e190 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -248,8 +248,6 @@ 此操作会覆盖当前设置。 显示信息 收藏 - 最近观看 - 最多观看 每次询问 新建播放列表 重命名 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index de343d5e75c..11595673d7f 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -320,8 +320,6 @@ 搞掂 NewPipe 專案非常著重你嘅私隱。因此,呢個 app 未得你同意係唔會收集任何資料。 \nNewPipe 嘅私隱政策會詳述,當你傳送彈 app 報告嗰陣,有咩資料會傳送同保存。 - 最近播放 - 最常播放 頭條新嘢 頭版要擺放邊啲分頁 打橫掃走啲項目去剷走佢 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5dcc6b0da82..80f19cd0076 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -216,8 +216,6 @@ 建立 退出 重新命名 - 上一次播放 - 最常播放 總是詢問 新的播放清單 重新命名 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 011c69fcb11..983b89517c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,7 @@ Clear watch history Deletes the history of played streams and the playback positions Delete entire watch history? + Your watching history will be permanently erased Watch history deleted Delete playback positions Deletes all playback positions @@ -389,8 +390,8 @@ History History Do you want to delete this item from search history? - Last Played - Most Played + Date + Views Content of main page What tabs are shown on the main page @@ -860,6 +861,10 @@ The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore. Next NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently. + Sort by + Button to clear watch history + Thumbnail for playlist %1$s + Thumbnail for stream %1$s %d comment %d comments diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8647d813047..b1c3d1e56d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,44 +3,44 @@ aboutLibraries = "11.2.3" acraCore = "5.11.3" androidState = "1.4.1" androidx-junit = "1.1.5" -appcompat = "1.6.1" +appcompat = "1.7.0" assertjCore = "3.24.2" auto-service = "1.1.1" bridge = "2.0.2" cardview = "1.0.0" checkstyle = "10.12.1" -coil = "3.0.4" +coil = "3.1.0" constraintlayout = "2.1.4" -core-ktx = "1.12.0" +core-ktx = "1.15.0" desugar-jdk-libs-nio = "2.0.4" documentFile = "1.0.1" exoplayer = "2.18.7" -fragment-compose = "1.8.2" -gradle = "8.7.1" +fragment-compose = "1.8.6" +gradle = "8.7.3" groupie = "2.10.1" -hilt = "2.51.1" -jetpack-compose = "2024.10.01" +hilt = "2.55" +jetpack-compose = "2025.04.00" jsoup = "1.17.2" junit = "4.13.2" -kotlin = "2.0.21" -kotlinxCoroutinesRx3 = "1.8.1" +kotlin = "2.1.10" +kotlinxCoroutinesRx3 = "1.10.1" kotlinxSerializationJson = "1.7.3" ktlint = "0.45.2" lazycolumnscrollbar = "2.2.0" leakcanary = "2.12" -lifecycle = "2.6.2" +lifecycle = "2.8.7" markwon = "4.6.2" material = "1.11.0" media = "1.7.0" mockitoCore = "5.6.0" -navigationCompose = "2.8.3" +navigationCompose = "2.8.9" okhttp = "4.12.0" -pagingCompose = "3.3.2" +pagingCompose = "3.3.6" preference = "1.2.1" prettytime = "5.0.8.Final" processPhoenix = "2.1.2" recyclerview = "1.3.2" -room = "2.6.1" +room = "2.7.0" # Fixes compatibility issue with newer Kotlin versions runner = "1.5.2" rxandroid = "3.0.2" rxbinding = "4.0.0" @@ -106,6 +106,7 @@ androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "room" } androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } androidx-room-rxjava3 = { group = "androidx.room", name = "room-rxjava3", version.ref = "room" } androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }