From 169eff809ee1d6636f156071aacd2b3c5c0ee152 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 14 Jan 2025 10:03:42 +0530 Subject: [PATCH 01/37] Rewrite history fragment using Compose --- app/build.gradle | 1 + .../history/dao/StreamHistoryDAO.java | 56 ++- .../local/history/HistoryEntryAdapter.java | 108 ----- .../newpipe/local/history/HistoryFragment.kt | 35 ++ .../local/history/HistoryRecordManager.java | 4 - .../newpipe/local/history/HistoryViewModel.kt | 32 ++ .../schabi/newpipe/local/history/SortKey.kt | 6 + .../history/StatisticsPlaylistFragment.java | 396 ------------------ .../org/schabi/newpipe/settings/tabs/Tab.java | 6 +- .../newpipe/ui/components/items/ItemList.kt | 25 -- .../newpipe/ui/components/items/ItemUtils.kt | 30 ++ .../items/history/HistoryCardItem.kt | 79 ++++ .../items/history/HistoryGridItem.kt | 69 +++ .../items/history/HistoryListItem.kt | 76 ++++ .../components/items/history/HistoryUtils.kt | 25 ++ .../newpipe/ui/screens/HistoryScreen.kt | 212 ++++++++++ .../schabi/newpipe/util/NavigationHelper.java | 4 +- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 1 + 19 files changed, 613 insertions(+), 553 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt create mode 100644 app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt create mode 100644 app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt create mode 100644 app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt diff --git a/app/build.gradle b/app/build.gradle index 77cbdc5a477..c05aae2a7fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,6 +221,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/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..3edf92d4263 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,6 +1,19 @@ package org.schabi.newpipe.database.history.dao; +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; +import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; +import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; +import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; + import androidx.annotation.Nullable; +import androidx.paging.PagingSource; import androidx.room.Dao; import androidx.room.Query; import androidx.room.RewriteQueriesToDropUnusedColumns; @@ -13,18 +26,6 @@ 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; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -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 @@ -70,13 +71,35 @@ public Flowable> listByService(final int serviceId) { @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" + ) + public abstract 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 +107,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" + ) + public abstract PagingSource getHistoryOrderedByViewCount(); } 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..d91603e00be --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -0,0 +1,35 @@ +package org.schabi.newpipe.local.history + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.fragment.app.Fragment +import androidx.fragment.compose.content +import org.schabi.newpipe.R +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(color = MaterialTheme.colorScheme.background) { + HistoryScreen() + } + } + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.title_activity_history) + } +} 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..cdc5c6f593d 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 @@ -171,10 +171,6 @@ 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); } 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..4aabadf08ef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -0,0 +1,32 @@ +package org.schabi.newpipe.local.history + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.paging.Pager +import androidx.paging.PagingConfig +import kotlinx.coroutines.flow.flatMapLatest +import org.schabi.newpipe.App +import org.schabi.newpipe.NewPipeDatabase + +class HistoryViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { + private val historyDao = NewPipeDatabase.getInstance(App.instance).streamHistoryDAO() + + val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.MOST_PLAYED) + val historyItems = sortKey + .flatMapLatest { + Pager(PagingConfig(pageSize = 20)) { + when (it) { + SortKey.LAST_PLAYED -> historyDao.getHistoryOrderedByLastWatched() + SortKey.MOST_PLAYED -> historyDao.getHistoryOrderedByViewCount() + } + }.flow + } + + 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..d18b770cfef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt @@ -0,0 +1,6 @@ +package org.schabi.newpipe.local.history + +enum class SortKey { + LAST_PLAYED, + MOST_PLAYED +} 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 fac3580754a..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ /dev/null @@ -1,396 +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))) - .setAction( - StreamDialogDefaultEntry.START_HERE_ON_BACKGROUND, - (f, i) -> NavigationHelper.playOnBackgroundPlayer( - context, getPlayQueueStartingAt(item), true)) - .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/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/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt index 4562e17aff7..664886c23ad 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 @@ -3,7 +3,6 @@ package org.schabi.newpipe.ui.components.items import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope 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 @@ -13,10 +12,6 @@ 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.preference.PreferenceManager -import androidx.window.core.layout.WindowWidthSizeClass -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 @@ -92,23 +87,3 @@ fun ItemList( } } } - -@Composable -private fun determineItemViewMode(): ItemViewMode { - val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) - .getString(stringResource(R.string.list_view_mode_key), null) - val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO - - return when (viewMode) { - ItemViewMode.AUTO -> { - // Evaluate whether to use Grid based on screen real estate. - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - ItemViewMode.GRID - } else { - ItemViewMode.LIST - } - } - else -> viewMode - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt new file mode 100644 index 00000000000..67a03afb9c4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt @@ -0,0 +1,30 @@ +package org.schabi.newpipe.ui.components.items + +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.preference.PreferenceManager +import androidx.window.core.layout.WindowWidthSizeClass +import org.schabi.newpipe.R +import org.schabi.newpipe.info_list.ItemViewMode + +@Composable +fun determineItemViewMode(): ItemViewMode { + val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) + .getString(stringResource(R.string.list_view_mode_key), null) + val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO + + return when (viewMode) { + ItemViewMode.AUTO -> { + // Evaluate whether to use Grid based on screen real estate. + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { + ItemViewMode.GRID + } else { + ItemViewMode.LIST + } + } + else -> viewMode + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt new file mode 100644 index 00000000000..fd6f822b9a1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt @@ -0,0 +1,79 @@ +package org.schabi.newpipe.ui.components.items.history + +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.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.unit.dp +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.ui.components.items.stream.StreamMenu +import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail +import java.time.format.DateTimeFormatter + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HistoryCardItem( + entry: StreamStatisticsEntry, + dateTimeFormatter: DateTimeFormatter, + showProgress: Boolean, + isSelected: Boolean, + onClick: (StreamStatisticsEntry) -> Unit = {}, + onLongClick: (StreamStatisticsEntry) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + val stream = entry.toStreamInfoItem() + + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(entry) }, + onClick = { onClick(entry) } + ) + .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.orEmpty(), + style = MaterialTheme.typography.bodySmall + ) + + Text( + text = getHistoryDetail(entry, dateTimeFormatter), + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt new file mode 100644 index 00000000000..d955b06deae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt @@ -0,0 +1,69 @@ +package org.schabi.newpipe.ui.components.items.history + +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.ui.components.items.stream.StreamMenu +import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail +import java.time.format.DateTimeFormatter + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HistoryGridItem( + entry: StreamStatisticsEntry, + dateTimeFormatter: DateTimeFormatter, + showProgress: Boolean, + isSelected: Boolean = false, + isMini: Boolean = false, + onClick: (StreamStatisticsEntry) -> Unit = {}, + onLongClick: (StreamStatisticsEntry) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + val stream = entry.toStreamInfoItem() + + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(entry) }, + onClick = { onClick(entry) } + ) + .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 = entry.streamEntity.title, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Text(text = entry.streamEntity.uploader, style = MaterialTheme.typography.bodySmall) + + Text( + text = getHistoryDetail(entry, dateTimeFormatter), + style = MaterialTheme.typography.bodySmall + ) + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt new file mode 100644 index 00000000000..ee8d5c67f6d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt @@ -0,0 +1,76 @@ +package org.schabi.newpipe.ui.components.items.history + +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.foundation.layout.size +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.ui.components.items.stream.StreamMenu +import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail +import java.time.format.DateTimeFormatter + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HistoryListItem( + entry: StreamStatisticsEntry, + dateTimeFormatter: DateTimeFormatter, + showProgress: Boolean, + isSelected: Boolean, + onClick: (StreamStatisticsEntry) -> Unit = {}, + onLongClick: (StreamStatisticsEntry) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + val stream = entry.toStreamInfoItem() + + // Box serves as an anchor for the dropdown menu + Box( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(entry) }, + onClick = { onClick(entry) } + ) + .fillMaxWidth() + .padding(12.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + StreamThumbnail( + stream = stream, + showProgress = showProgress, + modifier = Modifier.size(width = 140.dp, height = 78.dp) + ) + + Column { + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall) + + Text( + text = getHistoryDetail(entry, dateTimeFormatter), + style = MaterialTheme.typography.bodySmall + ) + } + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt new file mode 100644 index 00000000000..1af4c3659af --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt @@ -0,0 +1,25 @@ +package org.schabi.newpipe.ui.components.items.history + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.platform.LocalContext +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.ServiceHelper +import java.time.format.DateTimeFormatter + +@Composable +internal fun getHistoryDetail( + entry: StreamStatisticsEntry, + dateTimeFormatter: DateTimeFormatter, +): String { + val context = LocalContext.current + + return rememberSaveable(entry) { + Localization.concatenateStrings( + Localization.shortViewCount(context, entry.watchCount), + dateTimeFormatter.format(entry.latestAccessDate), + ServiceHelper.getNameOfServiceById(entry.streamEntity.serviceId), + ) + } +} 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..2fff600c659 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -0,0 +1,212 @@ +package org.schabi.newpipe.ui.screens + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +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.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.window.core.layout.WindowWidthSizeClass +import my.nanihadesuka.compose.LazyVerticalGridScrollbar +import org.schabi.newpipe.R +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.info_list.ItemViewMode +import org.schabi.newpipe.ktx.findFragmentActivity +import org.schabi.newpipe.local.history.HistoryViewModel +import org.schabi.newpipe.local.history.SortKey +import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar +import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings +import org.schabi.newpipe.ui.components.items.determineItemViewMode +import org.schabi.newpipe.ui.components.items.history.HistoryCardItem +import org.schabi.newpipe.ui.components.items.history.HistoryGridItem +import org.schabi.newpipe.ui.components.items.history.HistoryListItem +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 +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { + val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() + val historyItems = viewModel.historyItems.collectAsLazyPagingItems() + val onSelectItem = viewModel::updateOrder + + HistoryScreen(historyItems, sortKey, onSelectItem) +} + +@Composable +private fun HistoryScreen( + items: LazyPagingItems, + sortKey: SortKey, + onSelectItem: (SortKey) -> Unit, +) { + val mode = determineItemViewMode() + val context = LocalContext.current + val onClick = remember { + { item: StreamStatisticsEntry -> + val fragmentManager = context.findFragmentActivity().supportFragmentManager + NavigationHelper.openVideoDetailFragment( + context, fragmentManager, item.streamEntity.serviceId, item.streamEntity.url, + item.streamEntity.title, null, false + ) + } + } + val dateTimeFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) } + + // Handle long clicks for stream items + // TODO: Adjust the menu display depending on where it was triggered + var selectedStream by remember { mutableStateOf(null) } + val onLongClick = remember { + { stream: StreamStatisticsEntry -> + selectedStream = stream + } + } + val onDismissPopup = remember { + { + selectedStream = null + } + } + + val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context) + + if (items.loadState.refresh is LoadState.NotLoading && items.itemCount == 0) { + EmptyStateComposable(EmptyStateSpec.NoVideos) + } else if (mode == ItemViewMode.GRID) { + val state = rememberLazyGridState() + + LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT + val minSize = if (isCompact) 150.dp else 250.dp + + LazyVerticalGrid(state = state, columns = GridCells.Adaptive(minSize)) { + item(span = { GridItemSpan(maxLineSpan) }) { + HistoryHeader(sortKey, onSelectItem) + } + + items(items.itemCount) { + val item = items[it]!! + val isSelected = selectedStream == item + + HistoryGridItem( + item, dateTimeFormatter, showProgress, isSelected, isCompact, onClick, + onLongClick, onDismissPopup + ) + } + } + } + } else { + val state = rememberLazyListState() + + LazyColumnThemedScrollbar(state = state) { + LazyColumn(state = state) { + item { + HistoryHeader(sortKey, onSelectItem) + } + + items(items.itemCount) { + val item = items[it]!! + val isSelected = selectedStream == item + + if (mode == ItemViewMode.CARD) { + HistoryCardItem( + item, dateTimeFormatter, showProgress, isSelected, onClick, onLongClick, + onDismissPopup + ) + } else { + HistoryListItem( + item, dateTimeFormatter, showProgress, isSelected, onClick, onLongClick, + onDismissPopup + ) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HistoryHeader( + sortKey: SortKey, + onSelectItem: (SortKey) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + val selected = when (sortKey) { + SortKey.MOST_PLAYED -> R.string.title_most_played + SortKey.LAST_PLAYED -> R.string.title_last_played + } + + ExposedDropdownMenuBox( + modifier = Modifier.padding(top = 12.dp, start = 12.dp), + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + TextField( + enabled = true, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = stringResource(selected), + readOnly = true, + onValueChange = {}, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + label = { Text(text = stringResource(R.string.history_sort_label)) } + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_most_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectItem(SortKey.MOST_PLAYED) + } + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_last_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectItem(SortKey.LAST_PLAYED) + } + ) + } + } +} 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 e05142c7ac5..a140bed1576 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -52,7 +52,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; @@ -557,7 +557,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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bcb98ca939..ebe2555b7a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -858,6 +858,7 @@ 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 %d comment %d comments diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5dc0379d4c..a9756ec579b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,6 +101,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" } From 77e254b2be384d0572f6c3cfa63d3f9d70578179 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 14 Jan 2025 12:44:30 +0530 Subject: [PATCH 02/37] Remove unused classes --- .../newpipe/local/LocalItemListAdapter.java | 79 ++++----- .../LocalStatisticStreamCardItemHolder.java | 13 -- .../LocalStatisticStreamGridItemHolder.java | 13 -- .../LocalStatisticStreamItemHolder.java | 161 ------------------ 4 files changed, 29 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java 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/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); - } - } -} From 3d9394e69be0936987084304b8916caad5f0362f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 14 Jan 2025 14:45:11 +0530 Subject: [PATCH 03/37] Reuse stream composables --- .../newpipe/local/history/HistoryViewModel.kt | 51 ++++++++++- .../newpipe/ui/components/items/Info.kt | 91 +++++++++++++++++++ .../newpipe/ui/components/items/ItemList.kt | 20 ++-- .../items/history/HistoryListItem.kt | 76 ---------------- .../components/items/history/HistoryUtils.kt | 25 ----- .../items/playlist/PlaylistListItem.kt | 12 +-- .../items/playlist/PlaylistThumbnail.kt | 4 +- .../StreamCardItem.kt} | 26 ++---- .../StreamGridItem.kt} | 28 +++--- .../items/stream/StreamItemPreviewProvider.kt | 13 +++ .../components/items/stream/StreamListItem.kt | 14 +-- .../ui/components/items/stream/StreamMenu.kt | 19 ++-- .../items/stream/StreamThumbnail.kt | 8 +- .../ui/components/items/stream/StreamUtils.kt | 68 -------------- .../ui/components/video/RelatedItems.kt | 43 ++++++++- .../newpipe/ui/screens/HistoryScreen.kt | 39 ++++---- 16 files changed, 264 insertions(+), 273 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt rename app/src/main/java/org/schabi/newpipe/ui/components/items/{history/HistoryCardItem.kt => stream/StreamCardItem.kt} (72%) rename app/src/main/java/org/schabi/newpipe/ui/components/items/{history/HistoryGridItem.kt => stream/StreamGridItem.kt} (64%) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamUtils.kt 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 index 4aabadf08ef..102e4f35399 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -1,15 +1,30 @@ package org.schabi.newpipe.local.history +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel import androidx.paging.Pager import androidx.paging.PagingConfig +import androidx.paging.map +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flatMapLatest -import org.schabi.newpipe.App +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.ServiceHelper +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle -class HistoryViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { - private val historyDao = NewPipeDatabase.getInstance(App.instance).streamHistoryDAO() +class HistoryViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle, +) : AndroidViewModel(application) { + private val historyDao = NewPipeDatabase.getInstance(getApplication()).streamHistoryDAO() + private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.MOST_PLAYED) val historyItems = sortKey @@ -21,11 +36,39 @@ class HistoryViewModel(private val savedStateHandle: SavedStateHandle) : ViewMod } }.flow } + .map { pagingData -> + pagingData.map { + val thumbnails = listOfNotNull( + it.streamEntity.thumbnailUrl?.let { + Image(it, -1, -1, Image.ResolutionLevel.UNKNOWN) + } + ) + val detail = getHistoryDetailText(it, dateTimeFormatter) + + Stream( + it.streamEntity.serviceId, it.streamEntity.url, it.streamEntity.title, + thumbnails, it.streamEntity.uploader, it.streamEntity.streamType, + it.streamEntity.uploaderUrl, it.streamEntity.duration, detail + ) + } + } + .flowOn(Dispatchers.IO) fun updateOrder(sortKey: SortKey) { savedStateHandle[ORDER_KEY] = sortKey } + fun getHistoryDetailText( + entry: StreamStatisticsEntry, + dateTimeFormatter: DateTimeFormatter, + ): String { + return Localization.concatenateStrings( + Localization.shortViewCount(getApplication(), entry.watchCount), + dateTimeFormatter.format(entry.latestAccessDate), + ServiceHelper.getNameOfServiceById(entry.streamEntity.serviceId), + ) + } + companion object { private const val ORDER_KEY = "order" } 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..2fd6213828a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt @@ -0,0 +1,91 @@ +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.model.StreamEntity +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +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.image.ImageStrategy +import java.util.concurrent.TimeUnit + +sealed class Info + +@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 = "", +) : Info(), Parcelable { + + constructor(item: StreamInfoItem) : this( + item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), + item.streamType, item.uploaderUrl, item.duration, getStreamDetailText(item) + ) + + constructor(entry: StreamEntity, detailText: String) : this( + entry.serviceId, entry.url, entry.title, + ImageStrategy.dbUrlToImageList(entry.thumbnailUrl), entry.uploader, + entry.streamType, entry.uploaderUrl, entry.duration, detailText + ) + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(serviceId, url, name, type) + item.duration = duration + item.uploaderName = uploaderName + item.uploaderUrl = uploaderUrl + item.thumbnails = thumbnails + return item + } + + companion object { + fun getStreamDetailText(stream: StreamInfoItem): String { + val context = App.instance + 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) + + return if (views.isEmpty()) { + date.orEmpty() + } else if (date.isNullOrEmpty()) { + views + } else { + "$views • $date" + } + } + } +} + +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 664886c23ad..4933aba045f 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 @@ -7,14 +7,12 @@ 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 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 @@ -25,19 +23,19 @@ import org.schabi.newpipe.util.NavigationHelper @Composable fun ItemList( - items: List, + items: List, mode: ItemViewMode = determineItemViewMode(), listHeader: LazyListScope.() -> 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 ) @@ -47,9 +45,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 } } @@ -74,12 +72,12 @@ fun ItemList( items(items.size) { val item = items[it] - if (item is StreamInfoItem) { + if (item is Stream) { val isSelected = selectedStream == item StreamListItem( item, showProgress, isSelected, onClick, onLongClick, onDismissPopup ) - } else if (item is PlaylistInfoItem) { + } else if (item is Playlist) { PlaylistListItem(item, onClick) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt deleted file mode 100644 index ee8d5c67f6d..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryListItem.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.schabi.newpipe.ui.components.items.history - -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.foundation.layout.size -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.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import org.schabi.newpipe.database.stream.StreamStatisticsEntry -import org.schabi.newpipe.ui.components.items.stream.StreamMenu -import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail -import java.time.format.DateTimeFormatter - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun HistoryListItem( - entry: StreamStatisticsEntry, - dateTimeFormatter: DateTimeFormatter, - showProgress: Boolean, - isSelected: Boolean, - onClick: (StreamStatisticsEntry) -> Unit = {}, - onLongClick: (StreamStatisticsEntry) -> Unit = {}, - onDismissPopup: () -> Unit = {} -) { - val stream = entry.toStreamInfoItem() - - // Box serves as an anchor for the dropdown menu - Box( - modifier = Modifier - .combinedClickable( - onLongClick = { onLongClick(entry) }, - onClick = { onClick(entry) } - ) - .fillMaxWidth() - .padding(12.dp) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - StreamThumbnail( - stream = stream, - showProgress = showProgress, - modifier = Modifier.size(width = 140.dp, height = 78.dp) - ) - - Column { - Text( - text = stream.name, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleSmall, - maxLines = 2 - ) - - Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall) - - Text( - text = getHistoryDetail(entry, dateTimeFormatter), - style = MaterialTheme.typography.bodySmall - ) - } - } - - StreamMenu(stream, isSelected, onDismissPopup) - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt deleted file mode 100644 index 1af4c3659af..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.schabi.newpipe.ui.components.items.history - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.platform.LocalContext -import org.schabi.newpipe.database.stream.StreamStatisticsEntry -import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.ServiceHelper -import java.time.format.DateTimeFormatter - -@Composable -internal fun getHistoryDetail( - entry: StreamStatisticsEntry, - dateTimeFormatter: DateTimeFormatter, -): String { - val context = LocalContext.current - - return rememberSaveable(entry) { - Localization.concatenateStrings( - Localization.shortViewCount(context, entry.watchCount), - dateTimeFormatter.format(entry.latestAccessDate), - ServiceHelper.getNameOfServiceById(entry.streamEntity.serviceId), - ) - } -} 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 f282f9030c0..ee3e800ccba 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(color = MaterialTheme.colorScheme.background) { 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..859ee153343 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 @@ -20,13 +20,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.ui.components.items.Playlist 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 ) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt similarity index 72% rename from app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt rename to app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt index fd6f822b9a1..21f423e5ce7 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryCardItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui.components.items.history +package org.schabi.newpipe.ui.components.items.stream import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -15,30 +15,24 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import org.schabi.newpipe.database.stream.StreamStatisticsEntry -import org.schabi.newpipe.ui.components.items.stream.StreamMenu -import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail -import java.time.format.DateTimeFormatter +import org.schabi.newpipe.ui.components.items.Stream @OptIn(ExperimentalFoundationApi::class) @Composable -fun HistoryCardItem( - entry: StreamStatisticsEntry, - dateTimeFormatter: DateTimeFormatter, +fun StreamCardItem( + stream: Stream, showProgress: Boolean, isSelected: Boolean, - onClick: (StreamStatisticsEntry) -> Unit = {}, - onLongClick: (StreamStatisticsEntry) -> Unit = {}, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, onDismissPopup: () -> Unit = {} ) { - val stream = entry.toStreamInfoItem() - Box { Column( modifier = Modifier .combinedClickable( - onLongClick = { onLongClick(entry) }, - onClick = { onClick(entry) } + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } ) .padding(top = 12.dp, start = 2.dp, end = 2.dp) ) { @@ -62,12 +56,12 @@ fun HistoryCardItem( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = stream.uploaderName.orEmpty(), + text = stream.uploaderName, style = MaterialTheme.typography.bodySmall ) Text( - text = getHistoryDetail(entry, dateTimeFormatter), + text = stream.detailText, style = MaterialTheme.typography.bodySmall ) } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt similarity index 64% rename from app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt rename to app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt index d955b06deae..069a9e280e4 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/history/HistoryGridItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt @@ -1,4 +1,4 @@ -package org.schabi.newpipe.ui.components.items.history +package org.schabi.newpipe.ui.components.items.stream import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -13,31 +13,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import org.schabi.newpipe.database.stream.StreamStatisticsEntry -import org.schabi.newpipe.ui.components.items.stream.StreamMenu -import org.schabi.newpipe.ui.components.items.stream.StreamThumbnail -import java.time.format.DateTimeFormatter +import org.schabi.newpipe.ui.components.items.Stream @OptIn(ExperimentalFoundationApi::class) @Composable -fun HistoryGridItem( - entry: StreamStatisticsEntry, - dateTimeFormatter: DateTimeFormatter, +fun StreamGridItem( + stream: Stream, showProgress: Boolean, isSelected: Boolean = false, isMini: Boolean = false, - onClick: (StreamStatisticsEntry) -> Unit = {}, - onLongClick: (StreamStatisticsEntry) -> Unit = {}, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, onDismissPopup: () -> Unit = {} ) { - val stream = entry.toStreamInfoItem() - Box { Column( modifier = Modifier .combinedClickable( - onLongClick = { onLongClick(entry) }, - onClick = { onClick(entry) } + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } ) .padding(12.dp) ) { @@ -50,16 +44,16 @@ fun HistoryGridItem( ) Text( - text = entry.streamEntity.title, + text = stream.name, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleSmall, maxLines = 2 ) - Text(text = entry.streamEntity.uploader, style = MaterialTheme.typography.bodySmall) + Text(text = stream.uploaderName, style = MaterialTheme.typography.bodySmall) Text( - text = getHistoryDetail(entry, dateTimeFormatter), + text = stream.detailText, style = MaterialTheme.typography.bodySmall ) } 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..9e57fe6ce28 --- /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, uploaderName = "Uploader"), + Stream(type = StreamType.LIVE_STREAM), + Stream(type = StreamType.AUDIO_LIVE_STREAM), + ) +} 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 ee6bde28d88..11d42e67d8a 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(color = MaterialTheme.colorScheme.background) { 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 2902aa66004..ae721276c4e 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 @@ -12,11 +12,11 @@ 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.items.Stream import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.SparseItemUtil import org.schabi.newpipe.util.external_communication.ShareUtils @@ -24,10 +24,11 @@ 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() @@ -38,7 +39,7 @@ fun StreamMenu( text = R.string.enqueue_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueOnPlayer(context, it) } } @@ -49,7 +50,7 @@ fun StreamMenu( text = R.string.enqueue_next_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueNextOnPlayer(context, it) } } @@ -61,7 +62,7 @@ fun StreamMenu( text = R.string.start_here_on_background, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnBackgroundPlayer(context, it, true) } } @@ -70,7 +71,7 @@ fun StreamMenu( text = R.string.start_here_on_popup, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnPopupPlayer(context, it, true) } } @@ -93,7 +94,7 @@ fun StreamMenu( 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( @@ -121,7 +122,7 @@ fun StreamMenu( text = R.string.mark_as_watched, onClick = { onDismissRequest() - streamViewModel.markAsWatched(stream) + streamViewModel.markAsWatched(info) } ) StreamMenuItem( @@ -132,7 +133,7 @@ fun StreamMenu( 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..b029841cff8 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 @@ -24,7 +24,7 @@ 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.util.Localization import org.schabi.newpipe.util.StreamTypeUtil import org.schabi.newpipe.util.image.ImageStrategy @@ -34,7 +34,7 @@ import kotlin.time.Duration.Companion.seconds @Composable fun StreamThumbnail( - stream: StreamInfoItem, + stream: Stream, showProgress: Boolean, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit @@ -50,7 +50,7 @@ fun StreamThumbnail( modifier = modifier ) - val isLive = StreamTypeUtil.isLiveStream(stream.streamType) + val isLive = StreamTypeUtil.isLiveStream(stream.type) Text( modifier = Modifier .padding(2.dp) @@ -71,7 +71,7 @@ fun StreamThumbnail( 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 3c6c49d356d..b54374915ae 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 @@ -23,18 +23,23 @@ import androidx.compose.ui.unit.dp import androidx.core.content.edit import androidx.preference.PreferenceManager import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +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.components.items.Playlist +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.emptystate.EmptyStateComposable import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID +import java.util.concurrent.TimeUnit +import org.schabi.newpipe.extractor.stream.StreamInfo as ExtractorStreamInfo @Composable -fun RelatedItems(info: StreamInfo) { +fun RelatedItems(info: ExtractorStreamInfo) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) val key = stringResource(R.string.auto_queue_key) // TODO: AndroidX DataStore might be a better option. @@ -43,7 +48,15 @@ fun RelatedItems(info: StreamInfo) { } ItemList( - items = info.relatedItems, + items = info.relatedItems.map { + if (it is StreamInfoItem) { + Stream(it) + } else if (it is PlaylistInfoItem) { + Playlist(it) + } else { + throw IllegalArgumentException() + } + }, mode = ItemViewMode.LIST, listHeader = { item { @@ -82,11 +95,31 @@ 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 private fun RelatedItemsPreview() { - val info = StreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0) + val info = ExtractorStreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0) info.relatedItems = listOf( StreamInfoItem(streamType = StreamType.NONE), StreamInfoItem(streamType = StreamType.LIVE_STREAM), 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 index 2fff600c659..891b1b55c00 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -20,6 +20,7 @@ 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.platform.LocalContext @@ -33,23 +34,21 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.window.core.layout.WindowWidthSizeClass import my.nanihadesuka.compose.LazyVerticalGridScrollbar import org.schabi.newpipe.R -import org.schabi.newpipe.database.stream.StreamStatisticsEntry import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.components.items.determineItemViewMode -import org.schabi.newpipe.ui.components.items.history.HistoryCardItem -import org.schabi.newpipe.ui.components.items.history.HistoryGridItem -import org.schabi.newpipe.ui.components.items.history.HistoryListItem +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 -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle @Composable fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { @@ -62,28 +61,26 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { @Composable private fun HistoryScreen( - items: LazyPagingItems, + items: LazyPagingItems, sortKey: SortKey, onSelectItem: (SortKey) -> Unit, ) { val mode = determineItemViewMode() val context = LocalContext.current val onClick = remember { - { item: StreamStatisticsEntry -> + { item: Stream -> val fragmentManager = context.findFragmentActivity().supportFragmentManager NavigationHelper.openVideoDetailFragment( - context, fragmentManager, item.streamEntity.serviceId, item.streamEntity.url, - item.streamEntity.title, null, false + context, fragmentManager, item.serviceId, item.url, item.name, null, false ) } } - val dateTimeFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) } // 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: StreamStatisticsEntry -> + { stream: Stream -> selectedStream = stream } } @@ -114,9 +111,9 @@ private fun HistoryScreen( val item = items[it]!! val isSelected = selectedStream == item - HistoryGridItem( - item, dateTimeFormatter, showProgress, isSelected, isCompact, onClick, - onLongClick, onDismissPopup + StreamGridItem( + item, showProgress, isSelected, isCompact, onClick, onLongClick, + onDismissPopup ) } } @@ -135,14 +132,12 @@ private fun HistoryScreen( val isSelected = selectedStream == item if (mode == ItemViewMode.CARD) { - HistoryCardItem( - item, dateTimeFormatter, showProgress, isSelected, onClick, onLongClick, - onDismissPopup + StreamCardItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup ) } else { - HistoryListItem( - item, dateTimeFormatter, showProgress, isSelected, onClick, onLongClick, - onDismissPopup + StreamListItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup ) } } From f99fc13326b72becf7a86e20a284705b6833fb1c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 15 Jan 2025 07:28:42 +0530 Subject: [PATCH 04/37] Reuse ItemList composable --- .../newpipe/local/history/HistoryViewModel.kt | 8 +- .../newpipe/ui/components/items/Info.kt | 34 +---- .../newpipe/ui/components/items/ItemList.kt | 75 +++++++++-- .../ui/components/video/RelatedItems.kt | 107 +++++++++------- .../newpipe/ui/screens/HistoryScreen.kt | 119 +----------------- 5 files changed, 139 insertions(+), 204 deletions(-) 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 index 102e4f35399..6f32d431441 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -12,10 +12,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.database.stream.StreamStatisticsEntry -import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.image.ImageStrategy import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -38,11 +38,7 @@ class HistoryViewModel( } .map { pagingData -> pagingData.map { - val thumbnails = listOfNotNull( - it.streamEntity.thumbnailUrl?.let { - Image(it, -1, -1, Image.ResolutionLevel.UNKNOWN) - } - ) + val thumbnails = ImageStrategy.dbUrlToImageList(it.streamEntity.thumbnailUrl) val detail = getHistoryDetailText(it, dateTimeFormatter) Stream( 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 index 2fd6213828a..65f422b56f6 100644 --- 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 @@ -2,13 +2,11 @@ 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.model.StreamEntity import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem 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.image.ImageStrategy import java.util.concurrent.TimeUnit @@ -28,9 +26,9 @@ class Stream( val detailText: String = "", ) : Info(), Parcelable { - constructor(item: StreamInfoItem) : this( + constructor(item: StreamInfoItem, detailText: String) : this( item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), - item.streamType, item.uploaderUrl, item.duration, getStreamDetailText(item) + item.streamType, item.uploaderUrl, item.duration, detailText ) constructor(entry: StreamEntity, detailText: String) : this( @@ -47,32 +45,6 @@ class Stream( item.thumbnails = thumbnails return item } - - companion object { - fun getStreamDetailText(stream: StreamInfoItem): String { - val context = App.instance - 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) - - return if (views.isEmpty()) { - date.orEmpty() - } else if (date.isNullOrEmpty()) { - views - } else { - "$views • $date" - } - } - } } class Playlist( @@ -89,3 +61,5 @@ class Playlist( item.streamCount ) } + +object Unknown : Info() 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 4933aba045f..24f3bc65749 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,8 +1,12 @@ package org.schabi.newpipe.ui.components.items 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 @@ -13,19 +17,29 @@ 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.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.window.core.layout.WindowWidthSizeClass +import my.nanihadesuka.compose.LazyVerticalGridScrollbar 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.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 { @@ -60,23 +74,64 @@ 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(EmptyStateSpec.NoVideos) + } else if (mode == ItemViewMode.GRID) { + val state = rememberLazyGridState() + + LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT + val minSize = if (isCompact) 150.dp else 250.dp + + LazyVerticalGrid( + modifier = nestedScrollModifier, + state = state, + columns = GridCells.Adaptive(minSize) + ) { + 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 { val state = rememberLazyListState() LazyColumnThemedScrollbar(state = state) { LazyColumn(modifier = nestedScrollModifier, state = state) { - listHeader() + item { + header() + } - items(items.size) { + items(items.itemCount) { val item = items[it] + // TODO: Implement playlist and channel items. if (item is Stream) { val isSelected = selectedStream == item - StreamListItem( - item, showProgress, isSelected, onClick, onLongClick, onDismissPopup - ) + + 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) } 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 b54374915ae..4cdeb4c4b82 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 @@ -1,5 +1,6 @@ package org.schabi.newpipe.ui.components.video +import android.content.Context import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -21,80 +22,100 @@ 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.Playlist import org.schabi.newpipe.ui.components.items.Stream -import org.schabi.newpipe.ui.emptystate.EmptyStateComposable -import org.schabi.newpipe.ui.emptystate.EmptyStateSpec +import org.schabi.newpipe.ui.components.items.Unknown import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NO_SERVICE_ID import java.util.concurrent.TimeUnit -import org.schabi.newpipe.extractor.stream.StreamInfo as ExtractorStreamInfo @Composable -fun RelatedItems(info: ExtractorStreamInfo) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) +fun RelatedItems(info: StreamInfo) { + 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.map { + if (it is StreamInfoItem) { + Stream(it, getStreamDetailText(context, it)) + } else if (it is PlaylistInfoItem) { + Playlist(it) + } else { + Unknown + } + } ItemList( - items = info.relatedItems.map { - if (it is StreamInfoItem) { - Stream(it) - } else if (it is PlaylistInfoItem) { - Playlist(it) - } else { - throw IllegalArgumentException() - } - }, + 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(EmptyStateSpec.NoVideos) + } + ) } } } ) } +private fun getStreamDetailText(context: Context, stream: StreamInfoItem): String { + 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) + + return if (views.isEmpty()) { + date.orEmpty() + } else if (date.isNullOrEmpty()) { + views + } else { + "$views • $date" + } +} + private fun StreamInfoItem( serviceId: Int = NO_SERVICE_ID, url: String = "", @@ -119,7 +140,7 @@ private fun StreamInfoItem( @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun RelatedItemsPreview() { - val info = ExtractorStreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0) + val info = StreamInfo(NO_SERVICE_ID, "", "", StreamType.VIDEO_STREAM, "", "", 0) info.relatedItems = listOf( StreamInfoItem(streamType = StreamType.NONE), StreamInfoItem(streamType = StreamType.LIVE_STREAM), 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 index 891b1b55c00..8d7bf132cf3 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -1,12 +1,6 @@ package org.schabi.newpipe.ui.screens import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -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.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -15,135 +9,30 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Text import androidx.compose.material3.TextField -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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import androidx.window.core.layout.WindowWidthSizeClass -import my.nanihadesuka.compose.LazyVerticalGridScrollbar import org.schabi.newpipe.R -import org.schabi.newpipe.info_list.ItemViewMode -import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey -import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar -import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings -import org.schabi.newpipe.ui.components.items.Stream -import org.schabi.newpipe.ui.components.items.determineItemViewMode -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 +import org.schabi.newpipe.ui.components.items.ItemList @Composable fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() val historyItems = viewModel.historyItems.collectAsLazyPagingItems() - val onSelectItem = viewModel::updateOrder - HistoryScreen(historyItems, sortKey, onSelectItem) -} - -@Composable -private fun HistoryScreen( - items: LazyPagingItems, - sortKey: SortKey, - onSelectItem: (SortKey) -> Unit, -) { - val mode = determineItemViewMode() - val context = LocalContext.current - val onClick = remember { - { item: Stream -> - val fragmentManager = context.findFragmentActivity().supportFragmentManager - NavigationHelper.openVideoDetailFragment( - context, fragmentManager, item.serviceId, item.url, item.name, null, false - ) - } - } - - // Handle long clicks for stream items - // TODO: Adjust the menu display depending on where it was triggered - var selectedStream by rememberSaveable { mutableStateOf(null) } - val onLongClick = remember { - { stream: Stream -> - selectedStream = stream - } - } - val onDismissPopup = remember { - { - selectedStream = null - } - } - - val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context) - - if (items.loadState.refresh is LoadState.NotLoading && items.itemCount == 0) { - EmptyStateComposable(EmptyStateSpec.NoVideos) - } else if (mode == ItemViewMode.GRID) { - val state = rememberLazyGridState() - - LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT - val minSize = if (isCompact) 150.dp else 250.dp - - LazyVerticalGrid(state = state, columns = GridCells.Adaptive(minSize)) { - item(span = { GridItemSpan(maxLineSpan) }) { - HistoryHeader(sortKey, onSelectItem) - } - - items(items.itemCount) { - val item = items[it]!! - val isSelected = selectedStream == item - - StreamGridItem( - item, showProgress, isSelected, isCompact, onClick, onLongClick, - onDismissPopup - ) - } - } - } - } else { - val state = rememberLazyListState() - - LazyColumnThemedScrollbar(state = state) { - LazyColumn(state = state) { - item { - HistoryHeader(sortKey, onSelectItem) - } - - items(items.itemCount) { - val item = items[it]!! - val isSelected = selectedStream == item - - if (mode == ItemViewMode.CARD) { - StreamCardItem( - item, showProgress, isSelected, onClick, onLongClick, onDismissPopup - ) - } else { - StreamListItem( - item, showProgress, isSelected, onClick, onLongClick, onDismissPopup - ) - } - } - } - } - } + ItemList(historyItems, header = { + HistoryHeader(sortKey, viewModel::updateOrder) + }) } @OptIn(ExperimentalMaterial3Api::class) From 4ec5492814a9f5b231a8b0daff66d2a3884da5ae Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 15 Jan 2025 07:31:32 +0530 Subject: [PATCH 05/37] Update dependencies --- app/build.gradle | 2 +- gradle/libs.versions.toml | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c05aae2a7fc..63a3bcd1518 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ plugins { } android { - compileSdk 34 + compileSdk 35 namespace 'org.schabi.newpipe' defaultConfig { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9756ec579b..2bc655df368 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ 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" @@ -11,31 +11,31 @@ cardview = "1.0.0" checkstyle = "10.12.1" coil = "3.0.4" 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.5" +gradle = "8.7.3" groupie = "2.10.1" hilt = "2.51.1" -jetpack-compose = "2024.10.01" +jetpack-compose = "2024.12.01" jsoup = "1.17.2" junit = "4.13.2" -kotlin = "2.0.21" -kotlinxCoroutinesRx3 = "1.8.1" +kotlin = "2.1.0" +kotlinxCoroutinesRx3 = "1.9.0" ktlint = "0.45.2" lazycolumnscrollbar = "2.2.0" leakcanary = "2.12" -lifecycle = "2.6.2" +lifecycle = "2.8.7" localbroadcastmanager = "1.1.0" markwon = "4.6.2" material = "1.11.0" media = "1.7.0" mockitoCore = "5.6.0" -navigationCompose = "2.8.3" +navigationCompose = "2.8.5" okhttp = "4.12.0" -pagingCompose = "3.3.2" +pagingCompose = "3.3.5" preference = "1.2.1" prettytime = "5.0.8.Final" processPhoenix = "2.1.2" From 38a533f9a81de1d2078a0672cf0171e5c4fe12ff Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 15 Jan 2025 07:48:56 +0530 Subject: [PATCH 06/37] Combine ItemList code --- .../newpipe/ui/components/items/ItemList.kt | 23 ++++++++++++++ .../newpipe/ui/components/items/ItemUtils.kt | 30 ------------------- 2 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt 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 24f3bc65749..482c960d5f5 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 @@ -17,11 +17,14 @@ 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.info_list.ItemViewMode import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar @@ -140,3 +143,23 @@ fun ItemList( } } } + +@Composable +private fun determineItemViewMode(): ItemViewMode { + val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) + .getString(stringResource(R.string.list_view_mode_key), null) + val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO + + return when (viewMode) { + ItemViewMode.AUTO -> { + // Evaluate whether to use Grid based on screen real estate. + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { + ItemViewMode.GRID + } else { + ItemViewMode.LIST + } + } + else -> viewMode + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt deleted file mode 100644 index 67a03afb9c4..00000000000 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.schabi.newpipe.ui.components.items - -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.preference.PreferenceManager -import androidx.window.core.layout.WindowWidthSizeClass -import org.schabi.newpipe.R -import org.schabi.newpipe.info_list.ItemViewMode - -@Composable -fun determineItemViewMode(): ItemViewMode { - val prefValue = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) - .getString(stringResource(R.string.list_view_mode_key), null) - val viewMode = prefValue?.let { ItemViewMode.valueOf(it.uppercase()) } ?: ItemViewMode.AUTO - - return when (viewMode) { - ItemViewMode.AUTO -> { - // Evaluate whether to use Grid based on screen real estate. - val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - if (windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) { - ItemViewMode.GRID - } else { - ItemViewMode.LIST - } - } - else -> viewMode - } -} From 3f1bd587510edbae7a1d7bc4e71d5fa338d2d2fd Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 15 Jan 2025 08:13:29 +0530 Subject: [PATCH 07/37] Rm unused file --- app/src/main/res/menu/menu_history.xml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 app/src/main/res/menu/menu_history.xml diff --git a/app/src/main/res/menu/menu_history.xml b/app/src/main/res/menu/menu_history.xml deleted file mode 100644 index a0a3e78af0a..00000000000 --- a/app/src/main/res/menu/menu_history.xml +++ /dev/null @@ -1,11 +0,0 @@ -

- - - From 48ad12375bc3e13b60accf81a52aceae677a880c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 15 Jan 2025 08:49:14 +0530 Subject: [PATCH 08/37] Implement clear history functionality --- .../history/dao/StreamHistoryDAO.java | 33 ++--- .../local/history/HistoryRecordManager.java | 5 +- .../newpipe/local/history/HistoryViewModel.kt | 30 ++-- .../settings/HistorySettingsFragment.java | 2 +- .../newpipe/ui/screens/HistoryScreen.kt | 129 ++++++++++++------ app/src/main/res/values/strings.xml | 1 + 6 files changed, 111 insertions(+), 89 deletions(-) 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 3edf92d4263..cbda8eb4149 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 @@ -15,6 +15,8 @@ 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; @@ -24,36 +26,19 @@ import java.util.List; +import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; @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(); +public abstract class StreamHistoryDAO { + @Insert + public abstract long insert(StreamHistoryEntity entity); - @Override - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) - public abstract Flowable> getAll(); + @Delete + public abstract void delete(StreamHistoryEntity entity); - @Override @Query("DELETE FROM " + STREAM_HISTORY_TABLE) - public abstract int deleteAll(); - - @Override - public Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @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(); - + public abstract Completable deleteAll(); @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " + STREAM_HISTORY_TABLE 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 cdc5c6f593d..29b5ceacf9f 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 @@ -157,9 +157,8 @@ public Completable deleteStreamHistoryAndState(final long streamId) { }).subscribeOn(Schedulers.io()); } - public Single deleteWholeStreamHistory() { - return Single.fromCallable(streamHistoryTable::deleteAll) - .subscribeOn(Schedulers.io()); + public Completable deleteWholeStreamHistory() { + return streamHistoryTable.deleteAll().subscribeOn(Schedulers.io()); } public Single deleteCompleteStreamStateHistory() { 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 index 6f32d431441..610ed29e2b0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -3,6 +3,7 @@ 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.map @@ -10,12 +11,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await import org.schabi.newpipe.NewPipeDatabase -import org.schabi.newpipe.database.stream.StreamStatisticsEntry import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.ServiceHelper -import org.schabi.newpipe.util.image.ImageStrategy import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -38,14 +39,12 @@ class HistoryViewModel( } .map { pagingData -> pagingData.map { - val thumbnails = ImageStrategy.dbUrlToImageList(it.streamEntity.thumbnailUrl) - val detail = getHistoryDetailText(it, dateTimeFormatter) - - Stream( - it.streamEntity.serviceId, it.streamEntity.url, it.streamEntity.title, - thumbnails, it.streamEntity.uploader, it.streamEntity.streamType, - it.streamEntity.uploaderUrl, it.streamEntity.duration, detail + val detail = Localization.concatenateStrings( + Localization.shortViewCount(getApplication(), it.watchCount), + dateTimeFormatter.format(it.latestAccessDate), + ServiceHelper.getNameOfServiceById(it.streamEntity.serviceId), ) + Stream(it.streamEntity, detail) } } .flowOn(Dispatchers.IO) @@ -54,15 +53,10 @@ class HistoryViewModel( savedStateHandle[ORDER_KEY] = sortKey } - fun getHistoryDetailText( - entry: StreamStatisticsEntry, - dateTimeFormatter: DateTimeFormatter, - ): String { - return Localization.concatenateStrings( - Localization.shortViewCount(getApplication(), entry.watchCount), - dateTimeFormatter.format(entry.latestAccessDate), - ServiceHelper.getNameOfServiceById(entry.streamEntity.serviceId), - ) + fun deleteWatchHistory() { + viewModelScope.launch(Dispatchers.IO) { + historyDao.deleteAll().await() + } } companion object { 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..bbfd705dd52 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -91,7 +91,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, 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 index 8d7bf132cf3..11880de1672 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -1,21 +1,34 @@ package org.schabi.newpipe.ui.screens +import android.content.res.Configuration +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +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 @@ -24,6 +37,7 @@ import org.schabi.newpipe.R import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey import org.schabi.newpipe.ui.components.items.ItemList +import org.schabi.newpipe.ui.theme.AppTheme @Composable fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { @@ -31,7 +45,7 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { val historyItems = viewModel.historyItems.collectAsLazyPagingItems() ItemList(historyItems, header = { - HistoryHeader(sortKey, viewModel::updateOrder) + HistoryHeader(sortKey, viewModel::updateOrder, viewModel::deleteWatchHistory) }) } @@ -39,7 +53,8 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { @Composable private fun HistoryHeader( sortKey: SortKey, - onSelectItem: (SortKey) -> Unit, + onSelectSortKey: (SortKey) -> Unit, + onClickClear: () -> Unit, ) { var expanded by remember { mutableStateOf(false) } val selected = when (sortKey) { @@ -47,50 +62,78 @@ private fun HistoryHeader( SortKey.LAST_PLAYED -> R.string.title_last_played } - ExposedDropdownMenuBox( - modifier = Modifier.padding(top = 12.dp, start = 12.dp), - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - TextField( - enabled = true, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), - value = stringResource(selected), - readOnly = true, - onValueChange = {}, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.textFieldColors(), - label = { Text(text = stringResource(R.string.history_sort_label)) } - ) - - ExposedDropdownMenu( + Row(verticalAlignment = Alignment.CenterVertically) { + ExposedDropdownMenuBox( + modifier = Modifier.padding(top = 12.dp, start = 12.dp), expanded = expanded, - onDismissRequest = { expanded = false }, + onExpandedChange = { expanded = it }, ) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_most_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - expanded = false - onSelectItem(SortKey.MOST_PLAYED) - } - ) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_last_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - expanded = false - onSelectItem(SortKey.LAST_PLAYED) - } + TextField( + enabled = true, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = stringResource(selected), + readOnly = true, + onValueChange = {}, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + label = { Text(text = stringResource(R.string.history_sort_label)) } ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_most_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectSortKey(SortKey.MOST_PLAYED) + } + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_last_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectSortKey(SortKey.LAST_PLAYED) + } + ) + } + } + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text(text = stringResource(R.string.clear_views_history_title)) } + }, + state = rememberTooltipState(), + ) { + IconButton(onClick = onClickClear) { + Icon( + imageVector = Icons.Default.ClearAll, + contentDescription = stringResource(R.string.clear_history_description), + ) + } + } + } +} + +@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(color = MaterialTheme.colorScheme.background) { + HistoryHeader(SortKey.MOST_PLAYED, {}, {}) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebe2555b7a2..fd580ce46f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -859,6 +859,7 @@ 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 %d comment %d comments From 38645b02dff95d7836c694104f16014970ca84f2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 20 Jan 2025 10:51:00 +0530 Subject: [PATCH 09/37] Implement playback action buttons --- .../components/common/IconButtonWithLabel.kt | 49 +++++ .../newpipe/ui/screens/HistoryScreen.kt | 177 ++++++++++++------ .../schabi/newpipe/util/NavigationHelper.java | 3 +- 3 files changed, 170 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt 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..234f05de044 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt @@ -0,0 +1,49 @@ +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.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.MaterialTheme +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(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(color = MaterialTheme.colorScheme.background) { + IconButtonWithLabel(Icons.Default.Info, R.string.name) {} + } + } +} 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 index 11880de1672..ecc8c05f55f 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -1,10 +1,18 @@ package org.schabi.newpipe.ui.screens import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.PlaylistPlay import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.PictureInPicture import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -27,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -34,18 +43,40 @@ 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.ktx.findFragmentActivity import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey +import org.schabi.newpipe.player.playqueue.SinglePlayQueue +import org.schabi.newpipe.ui.components.common.IconButtonWithLabel import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.theme.AppTheme +import org.schabi.newpipe.util.NavigationHelper @Composable fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { + val context = LocalContext.current val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() val historyItems = viewModel.historyItems.collectAsLazyPagingItems() + val queue = SinglePlayQueue(historyItems.itemSnapshotList.map { it!!.toStreamInfoItem() }, 0) + val onClickBackground = { + NavigationHelper.playOnBackgroundPlayer(context, queue, false) + } + val onClickPopup = { + NavigationHelper.playOnPopupPlayer(context, queue, false) + } + val onClickPlayAll = { + NavigationHelper.playOnMainPlayer(context.findFragmentActivity(), queue) + } ItemList(historyItems, header = { - HistoryHeader(sortKey, viewModel::updateOrder, viewModel::deleteWatchHistory) + HistoryHeader( + sortKey = sortKey, + onSelectSortKey = viewModel::updateOrder, + onClickClear = viewModel::deleteWatchHistory, + onClickBackground = onClickBackground, + onClickPlayAll = onClickPlayAll, + onClickPopup = onClickPopup, + ) }) } @@ -55,6 +86,9 @@ private fun HistoryHeader( sortKey: SortKey, onSelectSortKey: (SortKey) -> Unit, onClickClear: () -> Unit, + onClickBackground: () -> Unit, + onClickPlayAll: () -> Unit, + onClickPopup: () -> Unit ) { var expanded by remember { mutableStateOf(false) } val selected = when (sortKey) { @@ -62,68 +96,97 @@ private fun HistoryHeader( SortKey.LAST_PLAYED -> R.string.title_last_played } - Row(verticalAlignment = Alignment.CenterVertically) { - ExposedDropdownMenuBox( - modifier = Modifier.padding(top = 12.dp, start = 12.dp), - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - TextField( - enabled = true, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), - value = stringResource(selected), - readOnly = true, - onValueChange = {}, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.textFieldColors(), - label = { Text(text = stringResource(R.string.history_sort_label)) } - ) - - ExposedDropdownMenu( + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + ExposedDropdownMenuBox( expanded = expanded, - onDismissRequest = { expanded = false }, + onExpandedChange = { expanded = it }, ) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_most_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - expanded = false - onSelectSortKey(SortKey.MOST_PLAYED) - } - ) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_last_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, - onClick = { - expanded = false - onSelectSortKey(SortKey.LAST_PLAYED) - } + TextField( + enabled = true, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = stringResource(selected), + readOnly = true, + onValueChange = {}, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + label = { Text(text = stringResource(R.string.history_sort_label)) } ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_most_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectSortKey(SortKey.MOST_PLAYED) + } + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.title_last_played), + color = MaterialTheme.colorScheme.onBackground, + ) + }, + onClick = { + expanded = false + onSelectSortKey(SortKey.LAST_PLAYED) + } + ) + } } - } - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip { Text(text = stringResource(R.string.clear_views_history_title)) } - }, - state = rememberTooltipState(), - ) { - IconButton(onClick = onClickClear) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = stringResource(R.string.clear_history_description), - ) + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text(text = stringResource(R.string.clear_views_history_title)) } + }, + state = rememberTooltipState(), + ) { + IconButton(onClick = onClickClear) { + Icon( + imageVector = Icons.Default.ClearAll, + contentDescription = stringResource(R.string.clear_history_description), + ) + } } } + + Spacer(Modifier.height(12.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + IconButtonWithLabel( + icon = Icons.Default.Headphones, + label = R.string.controls_background_title, + onClick = onClickBackground + ) + + IconButtonWithLabel( + icon = Icons.AutoMirrored.Filled.PlaylistPlay, + label = R.string.play_all, + onClick = onClickPlayAll + ) + + IconButtonWithLabel( + icon = Icons.Default.PictureInPicture, + label = R.string.controls_popup_title, + onClick = onClickPopup + ) + } } } @@ -133,7 +196,7 @@ private fun HistoryHeader( private fun HistoryHeaderPreview() { AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - HistoryHeader(SortKey.MOST_PLAYED, {}, {}) + HistoryHeader(SortKey.MOST_PLAYED, {}, {}, {}, {}, {}) } } } 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 a140bed1576..dbbecd57f55 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; @@ -137,7 +136,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) { From fa98a92415e2f6d615bafb5b4dc8b3905a47a4aa Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 20 Jan 2025 11:08:57 +0530 Subject: [PATCH 10/37] Cache history in view model --- .../java/org/schabi/newpipe/local/history/HistoryViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 610ed29e2b0..9193dde4ee8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -6,6 +6,7 @@ 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 @@ -48,6 +49,7 @@ class HistoryViewModel( } } .flowOn(Dispatchers.IO) + .cachedIn(viewModelScope) fun updateOrder(sortKey: SortKey) { savedStateHandle[ORDER_KEY] = sortKey From 6621b7feaa5610fc7b83bd72c21e1801ba9763cc Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 20 Jan 2025 11:18:01 +0530 Subject: [PATCH 11/37] Fix NPE --- .../java/org/schabi/newpipe/ui/components/items/ItemList.kt | 2 +- .../main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 482c960d5f5..ec59111996d 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 @@ -97,7 +97,7 @@ fun ItemList( } items(items.itemCount) { - val item = items[it]!! + val item = items[it] val isSelected = selectedStream == item // TODO: Implement playlist and channel grid items. 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 index ecc8c05f55f..90b93a5f87a 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -57,7 +57,9 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { val context = LocalContext.current val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() val historyItems = viewModel.historyItems.collectAsLazyPagingItems() - val queue = SinglePlayQueue(historyItems.itemSnapshotList.map { it!!.toStreamInfoItem() }, 0) + + val streams = historyItems.itemSnapshotList.mapNotNull { it?.toStreamInfoItem() } + val queue = SinglePlayQueue(streams, 0) val onClickBackground = { NavigationHelper.playOnBackgroundPlayer(context, queue, false) } From aeb4548a5ab47a70b273d5b38209e9fa0ff46176 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 21 Jan 2025 05:59:01 +0530 Subject: [PATCH 12/37] Add delete button for history items --- .../newpipe/local/history/HistoryViewModel.kt | 2 +- .../schabi/newpipe/ui/components/items/Info.kt | 9 +++++---- .../components/items/stream/StreamCardItem.kt | 18 ++++++++++++++++++ .../components/items/stream/StreamGridItem.kt | 18 ++++++++++++++++++ .../ui/components/items/stream/StreamMenu.kt | 11 +++++++++++ .../schabi/newpipe/ui/screens/HistoryScreen.kt | 6 ++++-- .../newpipe/viewmodels/StreamViewModel.kt | 6 ++++++ 7 files changed, 63 insertions(+), 7 deletions(-) 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 index 9193dde4ee8..4a4f08def07 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -45,7 +45,7 @@ class HistoryViewModel( dateTimeFormatter.format(it.latestAccessDate), ServiceHelper.getNameOfServiceById(it.streamEntity.serviceId), ) - Stream(it.streamEntity, detail) + Stream(it.streamEntity, detail, it.streamId) } } .flowOn(Dispatchers.IO) 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 index 65f422b56f6..d49525877f8 100644 --- 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 @@ -24,6 +24,7 @@ class Stream( val uploaderUrl: String? = null, val duration: Long = TimeUnit.HOURS.toSeconds(1), val detailText: String = "", + val streamId: Long = -1, ) : Info(), Parcelable { constructor(item: StreamInfoItem, detailText: String) : this( @@ -31,10 +32,10 @@ class Stream( item.streamType, item.uploaderUrl, item.duration, detailText ) - constructor(entry: StreamEntity, detailText: String) : this( - entry.serviceId, entry.url, entry.title, - ImageStrategy.dbUrlToImageList(entry.thumbnailUrl), entry.uploader, - entry.streamType, entry.uploaderUrl, entry.duration, detailText + constructor(entity: StreamEntity, detailText: String, streamId: Long) : this( + entity.serviceId, entity.url, entity.title, + ImageStrategy.dbUrlToImageList(entity.thumbnailUrl), entity.uploader, + entity.streamType, entity.uploaderUrl, entity.duration, detailText, streamId ) fun toStreamInfoItem(): StreamInfoItem { 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 index 21f423e5ce7..c94e0e4ee29 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -9,13 +10,17 @@ 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 @@ -71,3 +76,16 @@ fun StreamCardItem( 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(color = MaterialTheme.colorScheme.background) { + 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 index 069a9e280e4..0f324b50e15 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -7,13 +8,17 @@ 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 @@ -61,3 +66,16 @@ fun StreamGridItem( 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(color = MaterialTheme.colorScheme.background) { + StreamGridItem(stream, showProgress = false, isSelected = false) + } + } +} 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 ae721276c4e..7810cbaa95d 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 @@ -76,6 +76,17 @@ fun StreamMenu( } } ) + + if (stream.streamId != -1L) { + StreamMenuItem( + text = R.string.delete, + onClick = { + onDismissRequest() + streamViewModel.deleteStreamHistory(stream.streamId) + } + ) + } + StreamMenuItem( text = R.string.download, onClick = { 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 index 90b93a5f87a..22cd278a181 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -3,6 +3,8 @@ package org.schabi.newpipe.ui.screens import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -82,7 +84,7 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { }) } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable private fun HistoryHeader( sortKey: SortKey, @@ -170,7 +172,7 @@ private fun HistoryHeader( Spacer(Modifier.height(12.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { IconButtonWithLabel( icon = Icons.Default.Headphones, label = R.string.controls_background_title, 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..fd706fb9a1f 100644 --- a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt @@ -23,4 +23,10 @@ class StreamViewModel(application: Application) : AndroidViewModel(application) historyRecordManager.markAsWatched(stream).await() } } + + fun deleteStreamHistory(streamId: Long) { + viewModelScope.launch { + historyRecordManager.deleteStreamHistoryAndState(streamId).await() + } + } } From 3943a879662a22f7444226a7cc39cd1c7a542c06 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 21 Jan 2025 06:23:02 +0530 Subject: [PATCH 13/37] Fix preview issue --- .../ui/components/items/stream/StreamItemPreviewProvider.kt | 6 +++--- .../schabi/newpipe/ui/components/items/stream/StreamMenu.kt | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) 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 index 9e57fe6ce28..e2f698ade77 100644 --- 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 @@ -6,8 +6,8 @@ import org.schabi.newpipe.ui.components.items.Stream internal class StreamItemPreviewProvider : PreviewParameterProvider { override val values = sequenceOf( - Stream(type = StreamType.NONE, uploaderName = "Uploader"), - Stream(type = StreamType.LIVE_STREAM), - Stream(type = StreamType.AUDIO_LIVE_STREAM), + 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/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt index 7810cbaa95d..f718cad63ea 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 @@ -30,10 +30,11 @@ fun StreamMenu( ) { 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) { StreamMenuItem( text = R.string.enqueue_stream, From bf0692377cd0582e68d6cadbda2164220c7b95f9 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 21 Jan 2025 11:05:33 +0530 Subject: [PATCH 14/37] Reuse dropdown composable --- .../components/common/DropdownTextMenuItem.kt | 21 +++++++ .../ui/components/items/stream/StreamMenu.kt | 61 +++++++------------ .../newpipe/ui/screens/HistoryScreen.kt | 20 ++---- 3 files changed, 48 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt 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..256bd577e0c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt @@ -0,0 +1,21 @@ +package org.schabi.newpipe.ui.components.common + +import androidx.annotation.StringRes +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource + +@Composable +fun DropdownTextMenuItem( + @StringRes text: Int, + onClick: () -> Unit +) { + DropdownMenuItem( + text = { + Text(text = stringResource(text), color = MaterialTheme.colorScheme.onBackground) + }, + onClick = onClick + ) +} 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 f718cad63ea..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,13 +1,8 @@ package org.schabi.newpipe.ui.components.items.stream -import androidx.annotation.StringRes import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -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 @@ -16,6 +11,7 @@ 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 @@ -36,59 +32,59 @@ fun StreamMenu( val streamViewModel = viewModel() if (playerHolder.isPlayQueueReady) { - StreamMenuItem( + DropdownTextMenuItem( text = R.string.enqueue_stream, onClick = { onDismissRequest() SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueOnPlayer(context, it) } - } + }, ) if (playerHolder.queuePosition < playerHolder.queueSize - 1) { - StreamMenuItem( + DropdownTextMenuItem( text = R.string.enqueue_next_stream, onClick = { onDismissRequest() SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueNextOnPlayer(context, it) } - } + }, ) } } - StreamMenuItem( + DropdownTextMenuItem( text = R.string.start_here_on_background, onClick = { onDismissRequest() SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnBackgroundPlayer(context, it, true) } - } + }, ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.start_here_on_popup, onClick = { onDismissRequest() SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnPopupPlayer(context, it, true) } - } + }, ) if (stream.streamId != -1L) { - StreamMenuItem( + DropdownTextMenuItem( text = R.string.delete, onClick = { onDismissRequest() streamViewModel.deleteStreamHistory(stream.streamId) - } + }, ) } - StreamMenuItem( + DropdownTextMenuItem( text = R.string.download, onClick = { onDismissRequest() @@ -100,9 +96,9 @@ fun StreamMenu( val fragmentManager = context.findFragmentActivity().supportFragmentManager downloadDialog.show(fragmentManager, "downloadDialog") } - } + }, ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.add_to_playlist, onClick = { onDismissRequest() @@ -111,33 +107,33 @@ fun StreamMenu( val tag = if (dialog is PlaylistAppendDialog) "append" else "create" dialog.show( context.findFragmentActivity().supportFragmentManager, - "StreamDialogEntry@${tag}_playlist" + "StreamDialogEntry@${tag}_playlist", ) } - } + }, ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.share, onClick = { onDismissRequest() ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails) - } + }, ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.open_in_browser, onClick = { onDismissRequest() ShareUtils.openUrlInBrowser(context, stream.url) - } + }, ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.mark_as_watched, onClick = { onDismissRequest() streamViewModel.markAsWatched(info) } ) - StreamMenuItem( + DropdownTextMenuItem( text = R.string.show_channel_details, onClick = { onDismissRequest() @@ -151,16 +147,3 @@ fun StreamMenu( ) } } - -@Composable -private fun StreamMenuItem( - @StringRes text: Int, - onClick: () -> Unit -) { - DropdownMenuItem( - text = { - Text(text = stringResource(text), color = MaterialTheme.colorScheme.onBackground) - }, - onClick = onClick - ) -} 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 index 22cd278a181..d00164e1fa5 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material.icons.automirrored.filled.PlaylistPlay import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material.icons.filled.Headphones import androidx.compose.material.icons.filled.PictureInPicture -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -49,6 +48,7 @@ import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey import org.schabi.newpipe.player.playqueue.SinglePlayQueue +import org.schabi.newpipe.ui.components.common.DropdownTextMenuItem import org.schabi.newpipe.ui.components.common.IconButtonWithLabel import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.theme.AppTheme @@ -127,25 +127,15 @@ private fun HistoryHeader( expanded = expanded, onDismissRequest = { expanded = false }, ) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_most_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, + DropdownTextMenuItem( + text = R.string.title_most_played, onClick = { expanded = false onSelectSortKey(SortKey.MOST_PLAYED) } ) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.title_last_played), - color = MaterialTheme.colorScheme.onBackground, - ) - }, + DropdownTextMenuItem( + text = R.string.title_last_played, onClick = { expanded = false onSelectSortKey(SortKey.LAST_PLAYED) From 21caa6ce13131465386592e6b5ccd383d2bcf517 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 25 Jan 2025 11:19:28 +0530 Subject: [PATCH 15/37] Update Compose BOM --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d9232b9b03..64683628d78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ fragment-compose = "1.8.5" gradle = "8.7.3" groupie = "2.10.1" hilt = "2.51.1" -jetpack-compose = "2024.12.01" +jetpack-compose = "2025.01.00" jsoup = "1.17.2" junit = "4.13.2" kotlin = "2.1.0" From 5abc03c4cf43b45651fbed2dc5a22d6621bf494f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 27 Jan 2025 07:35:41 +0530 Subject: [PATCH 16/37] Extract thumbnail into common composable --- .../ui/components/items/common/Thumbnail.kt | 70 +++++++++++++++++++ .../items/playlist/PlaylistThumbnail.kt | 58 ++++----------- .../items/stream/StreamThumbnail.kt | 50 +++++-------- app/src/main/res/values/strings.xml | 2 + 4 files changed, 100 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt 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/PlaylistThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt index 859ee153343..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,28 +1,17 @@ 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.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( @@ -30,37 +19,14 @@ fun PlaylistThumbnail( 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/StreamThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamThumbnail.kt index b029841cff8..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,33 +1,25 @@ 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.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 @@ -40,31 +32,21 @@ fun StreamThumbnail( 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.type) - 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() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 786ce3cc4ef..5d08a84fddf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -861,6 +861,8 @@ 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 From 61bb81f2d68b4fb35eb8a33c904512fe40aa40c2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 1 Feb 2025 05:25:48 +0530 Subject: [PATCH 17/37] Fix black theme, remove manual background definition --- .../newpipe/local/history/HistoryFragment.kt | 3 +- .../ui/components/about/LicenseDialog.kt | 35 +++++++------------ .../components/common/IconButtonWithLabel.kt | 3 +- .../components/items/stream/StreamCardItem.kt | 2 +- .../components/items/stream/StreamGridItem.kt | 2 +- .../newpipe/ui/emptystate/EmptyStateUtil.kt | 12 +------ .../schabi/newpipe/ui/screens/AboutScreen.kt | 2 +- .../newpipe/ui/screens/HistoryScreen.kt | 3 +- .../java/org/schabi/newpipe/ui/theme/Theme.kt | 5 ++- 9 files changed, 23 insertions(+), 44 deletions(-) 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 index d91603e00be..7a08a27ea6c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.fragment.app.Fragment import androidx.fragment.compose.content @@ -20,7 +19,7 @@ class HistoryFragment : Fragment() { savedInstanceState: Bundle?, ) = content { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { HistoryScreen() } } 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/IconButtonWithLabel.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt index 234f05de044..fede3518d4a 100644 --- 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 @@ -8,7 +8,6 @@ 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.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -42,7 +41,7 @@ fun IconButtonWithLabel( @Composable private fun IconButtonWithLabelPreview() { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { IconButtonWithLabel(Icons.Default.Info, R.string.name) {} } } 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 index c94e0e4ee29..67f2f9442fb 100644 --- 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 @@ -84,7 +84,7 @@ private fun StreamCardItemPreview( @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream ) { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + 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 index 0f324b50e15..3ae3b137989 100644 --- 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 @@ -74,7 +74,7 @@ private fun StreamGridItemPreview( @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream ) { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { StreamGridItem(stream, showProgress = false, isSelected = false) } } 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 2fced431fa9..668cad3cea2 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 @@ -2,10 +2,6 @@ package org.schabi.newpipe.ui.emptystate -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import org.schabi.newpipe.ui.theme.AppTheme @@ -18,13 +14,7 @@ fun ComposeView.setEmptyStateComposable( setViewCompositionStrategy(strategy) setContent { AppTheme { - CompositionLocalProvider( - LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) - ) { - EmptyStateComposable( - spec = spec - ) - } + EmptyStateComposable(spec = spec) } } } 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 index d00164e1fa5..ae6b8a12a42 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Surface @@ -189,7 +188,7 @@ private fun HistoryHeader( @Composable private fun HistoryHeaderPreview() { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { HistoryHeader(SortKey.MOST_PLAYED, {}, {}, {}, {}, {}) } } 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) { From 212c67838a0e2657bc9a469ae11c83e1125c7c1c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 1 Feb 2025 05:29:14 +0530 Subject: [PATCH 18/37] Remove manual text color specification --- .../newpipe/ui/components/common/DropdownTextMenuItem.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 index 256bd577e0c..6eeb81aec9f 100644 --- 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 @@ -2,7 +2,6 @@ package org.schabi.newpipe.ui.components.common import androidx.annotation.StringRes import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource @@ -13,9 +12,7 @@ fun DropdownTextMenuItem( onClick: () -> Unit ) { DropdownMenuItem( - text = { - Text(text = stringResource(text), color = MaterialTheme.colorScheme.onBackground) - }, + text = { Text(text = stringResource(text)) }, onClick = onClick ) } From ac34ada7c91cde1d734042989d1a22a23c415e2c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 1 Feb 2025 05:41:02 +0530 Subject: [PATCH 19/37] Make DropdownTextMenuItem non-restartable --- .../schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 6eeb81aec9f..0388c4b1852 100644 --- 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 @@ -4,9 +4,11 @@ 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 From 695c05bd8e6512aa4cf0d0862b5b28a1c7b36e8d Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 5 Feb 2025 08:51:58 +0530 Subject: [PATCH 20/37] Add confirmation dialog for clearing watch history --- .../newpipe/ui/screens/HistoryScreen.kt | 66 ++++++++++++++----- app/src/main/res/values/strings.xml | 1 + 2 files changed, 49 insertions(+), 18 deletions(-) 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 index ae6b8a12a42..82979044de5 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -6,15 +6,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistPlay import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material.icons.filled.Headphones import androidx.compose.material.icons.filled.PictureInPicture +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -24,6 +23,7 @@ import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults @@ -93,20 +93,20 @@ private fun HistoryHeader( onClickPlayAll: () -> Unit, onClickPopup: () -> Unit ) { - var expanded by remember { mutableStateOf(false) } - val selected = when (sortKey) { - SortKey.MOST_PLAYED -> R.string.title_most_played - SortKey.LAST_PLAYED -> R.string.title_last_played - } - Column( modifier = Modifier .fillMaxWidth() .padding(12.dp), - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, ) { Row(verticalAlignment = Alignment.CenterVertically) { + var expanded by remember { mutableStateOf(false) } + val selected = when (sortKey) { + SortKey.MOST_PLAYED -> R.string.title_most_played + SortKey.LAST_PLAYED -> R.string.title_last_played + } + ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, @@ -119,7 +119,7 @@ private fun HistoryHeader( onValueChange = {}, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), - label = { Text(text = stringResource(R.string.history_sort_label)) } + label = { Text(text = stringResource(R.string.history_sort_label)) }, ) ExposedDropdownMenu( @@ -131,14 +131,14 @@ private fun HistoryHeader( onClick = { expanded = false onSelectSortKey(SortKey.MOST_PLAYED) - } + }, ) DropdownTextMenuItem( text = R.string.title_last_played, onClick = { expanded = false onSelectSortKey(SortKey.LAST_PLAYED) - } + }, ) } } @@ -150,39 +150,69 @@ private fun HistoryHeader( }, state = rememberTooltipState(), ) { - IconButton(onClick = onClickClear) { + var openClearDialog by remember { mutableStateOf(false) } + + IconButton(onClick = { openClearDialog = true }) { Icon( imageVector = Icons.Default.ClearAll, contentDescription = stringResource(R.string.clear_history_description), ) } + + ClearHistoryDialog(openClearDialog, onClickClear, onDismissRequest = { openClearDialog = false }) } } - Spacer(Modifier.height(12.dp)) - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { IconButtonWithLabel( icon = Icons.Default.Headphones, label = R.string.controls_background_title, - onClick = onClickBackground + onClick = onClickBackground, ) IconButtonWithLabel( icon = Icons.AutoMirrored.Filled.PlaylistPlay, label = R.string.play_all, - onClick = onClickPlayAll + onClick = onClickPlayAll, ) IconButtonWithLabel( icon = Icons.Default.PictureInPicture, label = R.string.controls_popup_title, - onClick = onClickPopup + onClick = onClickPopup, ) } } } +@Composable +private fun ClearHistoryDialog( + openClearDialog: Boolean, + onClickClear: () -> Unit, + onDismissRequest: () -> Unit +) { + if (openClearDialog) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(R.string.delete_view_history_alert)) }, + text = { Text(text = stringResource(R.string.delete_view_history_description)) }, + confirmButton = { + TextButton(onClick = { + onClickClear() + onDismissRequest() + }) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } +} + @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/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d08a84fddf..cb351a4811b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -215,6 +215,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 From dbe30116e37897a491c1c8e35f69d68fd91e567d Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 9 Feb 2025 19:11:02 +0530 Subject: [PATCH 21/37] Remove Unknown class --- .../org/schabi/newpipe/ui/components/items/Info.kt | 2 -- .../newpipe/ui/components/video/RelatedItems.kt | 13 +++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) 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 index d49525877f8..7f497c939a8 100644 --- 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 @@ -62,5 +62,3 @@ class Playlist( item.streamCount ) } - -object Unknown : Info() 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 6a5989f1370..150cc47c0ff 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 @@ -35,7 +35,6 @@ import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.components.items.Playlist import org.schabi.newpipe.ui.components.items.Stream -import org.schabi.newpipe.ui.components.items.Unknown import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NO_SERVICE_ID @@ -50,13 +49,11 @@ fun RelatedItems(info: StreamInfo) { var isAutoQueueEnabled by rememberSaveable { mutableStateOf(sharedPreferences.getBoolean(key, false)) } - val displayItems = info.relatedItems.map { - if (it is StreamInfoItem) { - Stream(it, getStreamDetailText(context, it)) - } else if (it is PlaylistInfoItem) { - Playlist(it) - } else { - Unknown + val displayItems = info.relatedItems.mapNotNull { + when (it) { + is StreamInfoItem -> Stream(it, getStreamDetailText(context, it)) + is PlaylistInfoItem -> Playlist(it) + else -> null } } From e877997603e723b6f837a30c0beed29a61417470 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 6 Mar 2025 06:11:49 +0530 Subject: [PATCH 22/37] Improve constructors --- .../database/stream/StreamStatisticsEntry.kt | 12 --- .../newpipe/local/history/HistoryViewModel.kt | 16 +--- .../newpipe/ui/components/items/Info.kt | 42 ---------- .../newpipe/ui/components/items/Stream.kt | 83 +++++++++++++++++++ .../ui/components/video/RelatedItems.kt | 26 +----- 5 files changed, 85 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt 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/local/history/HistoryViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt index 4a4f08def07..c094ba8ea9d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -16,17 +16,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.ui.components.items.Stream -import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.ServiceHelper -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle class HistoryViewModel( application: Application, private val savedStateHandle: SavedStateHandle, ) : AndroidViewModel(application) { private val historyDao = NewPipeDatabase.getInstance(getApplication()).streamHistoryDAO() - private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.MOST_PLAYED) val historyItems = sortKey @@ -38,16 +33,7 @@ class HistoryViewModel( } }.flow } - .map { pagingData -> - pagingData.map { - val detail = Localization.concatenateStrings( - Localization.shortViewCount(getApplication(), it.watchCount), - dateTimeFormatter.format(it.latestAccessDate), - ServiceHelper.getNameOfServiceById(it.streamEntity.serviceId), - ) - Stream(it.streamEntity, detail, it.streamId) - } - } + .map { pagingData -> pagingData.map { Stream(it) } } .flowOn(Dispatchers.IO) .cachedIn(viewModelScope) 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 index 7f497c939a8..bbbff457da4 100644 --- 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 @@ -1,53 +1,11 @@ package org.schabi.newpipe.ui.components.items -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem -import org.schabi.newpipe.extractor.stream.StreamInfoItem -import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.util.NO_SERVICE_ID -import org.schabi.newpipe.util.image.ImageStrategy -import java.util.concurrent.TimeUnit sealed class Info -@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, detailText: String) : this( - item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), - item.streamType, item.uploaderUrl, item.duration, detailText - ) - - constructor(entity: StreamEntity, detailText: String, streamId: Long) : this( - entity.serviceId, entity.url, entity.title, - ImageStrategy.dbUrlToImageList(entity.thumbnailUrl), entity.uploader, - entity.streamType, entity.uploaderUrl, entity.duration, detailText, 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 - } -} - class Playlist( val serviceId: Int = NO_SERVICE_ID, val url: String = "", 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/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt index 150cc47c0ff..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 @@ -1,6 +1,5 @@ package org.schabi.newpipe.ui.components.video -import android.content.Context import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -36,7 +35,6 @@ import org.schabi.newpipe.ui.components.items.ItemList 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.Localization import org.schabi.newpipe.util.NO_SERVICE_ID import java.util.concurrent.TimeUnit @@ -51,7 +49,7 @@ fun RelatedItems(info: StreamInfo) { } val displayItems = info.relatedItems.mapNotNull { when (it) { - is StreamInfoItem -> Stream(it, getStreamDetailText(context, it)) + is StreamInfoItem -> Stream(it) is PlaylistInfoItem -> Playlist(it) else -> null } @@ -90,28 +88,6 @@ fun RelatedItems(info: StreamInfo) { ) } -private fun getStreamDetailText(context: Context, stream: StreamInfoItem): String { - 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) - - return if (views.isEmpty()) { - date.orEmpty() - } else if (date.isNullOrEmpty()) { - views - } else { - "$views • $date" - } -} - private fun StreamInfoItem( serviceId: Int = NO_SERVICE_ID, url: String = "", From 975ba3c067ab6c5f7ec600b89d3b507a26de4e4c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 8 Mar 2025 18:40:08 +0530 Subject: [PATCH 23/37] Reduce button size, fix alignment --- .../newpipe/ui/components/common/IconButtonWithLabel.kt | 6 +++++- .../java/org/schabi/newpipe/ui/screens/HistoryScreen.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) 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 index fede3518d4a..f3a23f62f2b 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -25,7 +26,10 @@ fun IconButtonWithLabel( @StringRes label: Int, onClick: () -> Unit, ) { - FilledTonalButton(onClick = onClick) { + FilledTonalButton( + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), + onClick = onClick + ) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, 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 index 82979044de5..fa3048b0b4f 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -163,7 +163,7 @@ private fun HistoryHeader( } } - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { IconButtonWithLabel( icon = Icons.Default.Headphones, label = R.string.controls_background_title, From 851ba4bf1dd15ce4898723c019121593feed0dcc Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 11 Mar 2025 10:58:42 +0530 Subject: [PATCH 24/37] Use segmented button for sort options --- .../schabi/newpipe/local/history/SortKey.kt | 9 +- .../newpipe/ui/screens/HistoryScreen.kt | 98 +++++-------------- app/src/main/res/values/strings.xml | 4 +- 3 files changed, 35 insertions(+), 76 deletions(-) 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 index d18b770cfef..4fb7abc8aaa 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt @@ -1,6 +1,9 @@ package org.schabi.newpipe.local.history -enum class SortKey { - LAST_PLAYED, - MOST_PLAYED +import androidx.annotation.StringRes +import org.schabi.newpipe.R + +enum class SortKey(@StringRes val title: Int) { + LAST_PLAYED(R.string.title_last_played), + MOST_PLAYED(R.string.title_most_played) } 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 index fa3048b0b4f..5cbaefbe447 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -2,7 +2,6 @@ package org.schabi.newpipe.ui.screens import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row @@ -15,19 +14,13 @@ import androidx.compose.material.icons.filled.Headphones import androidx.compose.material.icons.filled.PictureInPicture import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MenuAnchorType -import androidx.compose.material3.PlainTooltip +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.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TooltipBox -import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -47,7 +40,6 @@ import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.history.HistoryViewModel import org.schabi.newpipe.local.history.SortKey import org.schabi.newpipe.player.playqueue.SinglePlayQueue -import org.schabi.newpipe.ui.components.common.DropdownTextMenuItem import org.schabi.newpipe.ui.components.common.IconButtonWithLabel import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.theme.AppTheme @@ -93,73 +85,23 @@ private fun HistoryHeader( onClickPlayAll: () -> Unit, onClickPopup: () -> Unit ) { - Column( + FlowRow( modifier = Modifier .fillMaxWidth() .padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), ) { - Row(verticalAlignment = Alignment.CenterVertically) { - var expanded by remember { mutableStateOf(false) } - val selected = when (sortKey) { - SortKey.MOST_PLAYED -> R.string.title_most_played - SortKey.LAST_PLAYED -> R.string.title_last_played - } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - TextField( - enabled = true, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), - value = stringResource(selected), - readOnly = true, - onValueChange = {}, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.textFieldColors(), - label = { Text(text = stringResource(R.string.history_sort_label)) }, - ) - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, + SingleChoiceSegmentedButtonRow { + SortKey.entries.forEachIndexed { index, key -> + SegmentedButton( + selected = key == sortKey, + onClick = { onSelectSortKey(key) }, + shape = SegmentedButtonDefaults + .itemShape(index = index, count = SortKey.entries.size) ) { - DropdownTextMenuItem( - text = R.string.title_most_played, - onClick = { - expanded = false - onSelectSortKey(SortKey.MOST_PLAYED) - }, - ) - DropdownTextMenuItem( - text = R.string.title_last_played, - onClick = { - expanded = false - onSelectSortKey(SortKey.LAST_PLAYED) - }, - ) - } - } - - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip { Text(text = stringResource(R.string.clear_views_history_title)) } - }, - state = rememberTooltipState(), - ) { - var openClearDialog by remember { mutableStateOf(false) } - - IconButton(onClick = { openClearDialog = true }) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = stringResource(R.string.clear_history_description), - ) + Text(text = stringResource(key.title)) } - - ClearHistoryDialog(openClearDialog, onClickClear, onDismissRequest = { openClearDialog = false }) } } @@ -181,6 +123,20 @@ private fun HistoryHeader( label = R.string.controls_popup_title, onClick = onClickPopup, ) + + var openClearDialog by remember { mutableStateOf(false) } + + TextButton(onClick = { openClearDialog = true }) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Default.ClearAll, contentDescription = null) + Text(text = stringResource(R.string.clear)) + } + } + + ClearHistoryDialog(openClearDialog, onClickClear, onDismissRequest = { openClearDialog = false }) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 326090134c0..42850488a58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -389,8 +389,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 From 033d2880779e41339c88795f80d5ad13e89a7fa4 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 16 Mar 2025 12:48:58 +0530 Subject: [PATCH 25/37] Use "Sort By" label, remove outdated translations --- .../schabi/newpipe/local/history/SortKey.kt | 4 +- .../newpipe/ui/screens/HistoryScreen.kt | 128 ++++++++++-------- .../res/layout/statistic_playlist_control.xml | 2 +- app/src/main/res/values-ar-rLY/strings.xml | 2 - app/src/main/res/values-ar/strings.xml | 3 - app/src/main/res/values-az/strings.xml | 2 - app/src/main/res/values-b+ast/strings.xml | 2 - app/src/main/res/values-b+uz+Latn/strings.xml | 2 - app/src/main/res/values-be/strings.xml | 2 - app/src/main/res/values-bg/strings.xml | 2 - app/src/main/res/values-bn-rBD/strings.xml | 2 - app/src/main/res/values-bn-rIN/strings.xml | 2 - app/src/main/res/values-bn/strings.xml | 2 - app/src/main/res/values-ca/strings.xml | 2 - app/src/main/res/values-ckb/strings.xml | 3 - app/src/main/res/values-cs/strings.xml | 2 - app/src/main/res/values-da/strings.xml | 2 - app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-el/strings.xml | 2 - app/src/main/res/values-eo/strings.xml | 2 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-et/strings.xml | 2 - app/src/main/res/values-eu/strings.xml | 2 - app/src/main/res/values-fa/strings.xml | 2 - app/src/main/res/values-fi/strings.xml | 2 - app/src/main/res/values-fil/strings.xml | 2 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values-gl/strings.xml | 2 - app/src/main/res/values-he/strings.xml | 2 - app/src/main/res/values-hi/strings.xml | 2 - app/src/main/res/values-hr/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-ia/strings.xml | 2 - app/src/main/res/values-in/strings.xml | 2 - app/src/main/res/values-is/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-ka/strings.xml | 2 - app/src/main/res/values-kab/strings.xml | 2 - app/src/main/res/values-kmr/strings.xml | 2 - app/src/main/res/values-ko/strings.xml | 2 - app/src/main/res/values-ku/strings.xml | 2 - app/src/main/res/values-lt/strings.xml | 2 - app/src/main/res/values-lv/strings.xml | 2 - app/src/main/res/values-mk/strings.xml | 2 - app/src/main/res/values-ml/strings.xml | 2 - app/src/main/res/values-ms/strings.xml | 2 - app/src/main/res/values-nb-rNO/strings.xml | 2 - app/src/main/res/values-ne/strings.xml | 2 - app/src/main/res/values-nl-rBE/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 2 - app/src/main/res/values-nqo/strings.xml | 2 - app/src/main/res/values-or/strings.xml | 2 - app/src/main/res/values-pa/strings.xml | 2 - app/src/main/res/values-pl/strings.xml | 2 - app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values-pt-rPT/strings.xml | 2 - app/src/main/res/values-pt/strings.xml | 2 - app/src/main/res/values-ro/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-ryu/strings.xml | 4 - app/src/main/res/values-sat/strings.xml | 2 - app/src/main/res/values-sc/strings.xml | 2 - app/src/main/res/values-sk/strings.xml | 2 - app/src/main/res/values-sl/strings.xml | 2 - app/src/main/res/values-so/strings.xml | 2 - app/src/main/res/values-sq/strings.xml | 2 - app/src/main/res/values-sr/strings.xml | 2 - app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-ta/strings.xml | 2 - app/src/main/res/values-te/strings.xml | 2 - app/src/main/res/values-th/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-uk/strings.xml | 2 - app/src/main/res/values-ur/strings.xml | 2 - app/src/main/res/values-vi/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values-zh-rHK/strings.xml | 2 - app/src/main/res/values-zh-rTW/strings.xml | 2 - app/src/main/res/values/strings.xml | 4 +- 80 files changed, 77 insertions(+), 217 deletions(-) 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 index 4fb7abc8aaa..b4bb6645dd6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt @@ -4,6 +4,6 @@ import androidx.annotation.StringRes import org.schabi.newpipe.R enum class SortKey(@StringRes val title: Int) { - LAST_PLAYED(R.string.title_last_played), - MOST_PLAYED(R.string.title_most_played) + LAST_PLAYED(R.string.history_sort_date), + MOST_PLAYED(R.string.history_sort_views) } 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 index 5cbaefbe447..803797d7df9 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material.icons.filled.Headphones import androidx.compose.material.icons.filled.PictureInPicture import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -75,7 +74,7 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { }) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable private fun HistoryHeader( sortKey: SortKey, @@ -92,6 +91,23 @@ private fun HistoryHeader( verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), ) { + HistorySortRow(sortKey, onSelectSortKey) + + HistoryButtons(onClickClear, onClickBackground, onClickPlayAll, onClickPopup) + } +} + +@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( @@ -104,68 +120,68 @@ private fun HistoryHeader( } } } + } +} - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { - IconButtonWithLabel( - icon = Icons.Default.Headphones, - label = R.string.controls_background_title, - onClick = onClickBackground, - ) +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun HistoryButtons( + onClickClear: () -> Unit, + onClickBackground: () -> Unit, + onClickPlayAll: () -> Unit, + onClickPopup: () -> Unit +) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { + IconButtonWithLabel( + icon = Icons.Default.Headphones, + label = R.string.controls_background_title, + onClick = onClickBackground, + ) - IconButtonWithLabel( - icon = Icons.AutoMirrored.Filled.PlaylistPlay, - label = R.string.play_all, - onClick = onClickPlayAll, - ) + IconButtonWithLabel( + icon = Icons.AutoMirrored.Filled.PlaylistPlay, + label = R.string.play_all, + onClick = onClickPlayAll, + ) - IconButtonWithLabel( - icon = Icons.Default.PictureInPicture, - label = R.string.controls_popup_title, - onClick = onClickPopup, - ) + IconButtonWithLabel( + icon = Icons.Default.PictureInPicture, + label = R.string.controls_popup_title, + onClick = onClickPopup, + ) - var openClearDialog by remember { mutableStateOf(false) } + var openClearDialog by remember { mutableStateOf(false) } - TextButton(onClick = { openClearDialog = true }) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = Icons.Default.ClearAll, contentDescription = null) - Text(text = stringResource(R.string.clear)) - } + TextButton(onClick = { openClearDialog = true }) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Default.ClearAll, contentDescription = null) + Text(text = stringResource(R.string.clear)) } - - ClearHistoryDialog(openClearDialog, onClickClear, onDismissRequest = { openClearDialog = false }) } - } -} -@Composable -private fun ClearHistoryDialog( - openClearDialog: Boolean, - onClickClear: () -> Unit, - onDismissRequest: () -> Unit -) { - if (openClearDialog) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.delete_view_history_alert)) }, - text = { Text(text = stringResource(R.string.delete_view_history_description)) }, - confirmButton = { - TextButton(onClick = { - onClickClear() - onDismissRequest() - }) { - Text(stringResource(R.string.delete)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(stringResource(R.string.cancel)) - } - }, - ) + if (openClearDialog) { + AlertDialog( + onDismissRequest = { openClearDialog = false }, + title = { Text(text = stringResource(R.string.delete_view_history_alert)) }, + text = { Text(text = stringResource(R.string.delete_view_history_description)) }, + confirmButton = { + TextButton(onClick = { + onClickClear() + openClearDialog = false + }) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + TextButton(onClick = { openClearDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } } } 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 3af148ce542..c06bf0930e4 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 9fcf10f7d90..da0870a9ab9 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 928b58f6627..d15511aec51 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 b15060e36c8..96a31923b91 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -231,8 +231,6 @@ Гісторыя Гісторыя Выдаліць гэты элемент з гісторыі пошуку? - Нядаўна прайграныя - Найбольш прайграваныя Кантэнт галоўнай старонкі Пустая старонка Старонка кіёска diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 3d746afced2..8f2f4563bab 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 0fd86b8e5ab..03f2dfff3b4 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 d7b93667e84..baab2409ac1 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 3ce9f331079..fb4746c77eb 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 48a160c9d05..6a8a31c31a4 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 0dbbcc516b3..02fb5fd2875 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 28bc57358f2..da70726312c 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 60aa0556d4e..92a005c7c94 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 36fe778f235..a222e75a90e 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 22ca848c090..7f6babec1d4 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 d6a39acd61d..fdb06a98855 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 60da52ae19c..ef172e23b0b 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 b8af4553948..6711d3e2990 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 4536b2f42d1..d5af18c2f84 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 7f68bcbfda6..70ce104e2c6 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 40512aa9f83..b99aa6e5642 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 5e00a9cc73c..496e545a401 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 68dce5bf1cb..0ef7c735448 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 d7eb334a8d3..0a91f73358c 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 6cdb831b4bd..64de5f3ebd4 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 ca77b363dc4..45e7d6e9e2c 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 c7260752b2e..f9dd77e1158 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 49ae4854f03..e876057e91c 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 6eb51bfde29..06f5d11549d 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 9415ef9f24a..872759bfe1d 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 92d26575865..8c43551484c 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 9864051d836..a74542e18b3 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 ed4bcb84d85..e1a74c07b1d 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 7855b8f2227..df51b97fab1 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 b40727c2a01..06343e373f8 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 21cd2609caf..d2fc3a4aeb6 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 7e5761a63d5..21974f1aeea 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 02a1c4f2178..f7230188a1f 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 1321123db8d..6e3b2615412 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 8d88613973b..47b2240a6ca 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 4b504936a31..18052b46c81 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 141ea2f3533..dd9c5cda7ca 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 f7ff02d5918..39f2c7e6e69 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 4d6f8c80a3b..c59c5be14e3 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 d1498cdd81b..fff45b04167 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 3e53afea733..c33ee9b497e 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 533bc6df8f0..58a8b5d5a27 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 958df593e6c..1d5c923dae3 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -606,8 +606,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 1816fa212f4..f68f44983ed 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 762e8c42f2e..0d45fe64541 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 7d9814144c8..bdd99ccb307 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 90397541d28..7f9fe727e76 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 582d69cdd6c..2e227556646 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 b8085480d7a..f62129dbca1 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 d07ffb624ad..93ee9081287 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 42850488a58..dc3f48acd03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -389,8 +389,8 @@ History History Do you want to delete this item from search history? - Date - Views + Date + Views Content of main page What tabs are shown on the main page From 2bafa2c715f4f53be68a4a14834e5510b81a713c Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 24 Mar 2025 06:16:45 +0530 Subject: [PATCH 26/37] Add loading indicator in ItemList --- .../schabi/newpipe/ui/components/items/ItemList.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 ec59111996d..6e5c49d219c 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,5 +1,7 @@ package org.schabi.newpipe.ui.components.items +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -28,6 +30,7 @@ import org.schabi.newpipe.R 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 @@ -85,12 +88,12 @@ fun ItemList( LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT - val minSize = if (isCompact) 150.dp else 250.dp + val minWidth = if (isCompact) 150.dp else 250.dp LazyVerticalGrid( modifier = nestedScrollModifier, state = state, - columns = GridCells.Adaptive(minSize) + columns = GridCells.Adaptive(minWidth) ) { item(span = { GridItemSpan(maxLineSpan) }) { header() @@ -106,6 +109,8 @@ fun ItemList( item, showProgress, isSelected, isCompact, onClick, onLongClick, onDismissPopup ) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.size(minWidth, if (isCompact) 150.dp else 200.dp)) } } } @@ -137,6 +142,8 @@ fun ItemList( } } else if (item is Playlist) { PlaylistListItem(item, onClick) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.height(80.dp)) } } } From 54c2b499af4afd8744ee2e9f98101035c9c9428a Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 25 Mar 2025 05:48:42 +0530 Subject: [PATCH 27/37] Restore clear menu --- .../newpipe/local/history/HistoryFragment.kt | 62 ++++++++++- .../newpipe/local/history/HistoryViewModel.kt | 8 -- .../common/PlaybackControlButtons.kt | 43 ++++++++ .../newpipe/ui/screens/HistoryScreen.kt | 104 +----------------- app/src/main/res/menu/menu_history.xml | 11 ++ gradle/libs.versions.toml | 2 +- 6 files changed, 122 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt create mode 100644 app/src/main/res/menu/menu_history.xml 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 index 7a08a27ea6c..b0575a3fc74 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -2,13 +2,26 @@ 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 @@ -29,6 +42,53 @@ class HistoryFragment : Fragment() { view: View, savedInstanceState: Bundle?, ) { - (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.title_activity_history) + 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("Clear orphaned records")) { + recordManager.deleteCompleteStreamStateHistory().await() + Toast + .makeText(context, R.string.watch_history_states_deleted, Toast.LENGTH_SHORT) + .show() + } + + launch(getExceptionHandler("Delete search 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/HistoryViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt index c094ba8ea9d..d1a64360ce7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -12,8 +12,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.await import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.ui.components.items.Stream @@ -41,12 +39,6 @@ class HistoryViewModel( savedStateHandle[ORDER_KEY] = sortKey } - fun deleteWatchHistory() { - viewModelScope.launch(Dispatchers.IO) { - historyDao.deleteAll().await() - } - } - companion object { private const val ORDER_KEY = "order" } 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/screens/HistoryScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt index 803797d7df9..55f44293811 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -7,27 +7,15 @@ 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.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.Headphones -import androidx.compose.material.icons.filled.PictureInPicture -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon 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.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -35,41 +23,26 @@ 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.ktx.findFragmentActivity 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.IconButtonWithLabel +import org.schabi.newpipe.ui.components.common.PlaybackControlButtons import org.schabi.newpipe.ui.components.items.ItemList import org.schabi.newpipe.ui.theme.AppTheme -import org.schabi.newpipe.util.NavigationHelper @Composable fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { - val context = LocalContext.current val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() val historyItems = viewModel.historyItems.collectAsLazyPagingItems() - val streams = historyItems.itemSnapshotList.mapNotNull { it?.toStreamInfoItem() } val queue = SinglePlayQueue(streams, 0) - val onClickBackground = { - NavigationHelper.playOnBackgroundPlayer(context, queue, false) - } - val onClickPopup = { - NavigationHelper.playOnPopupPlayer(context, queue, false) - } - val onClickPlayAll = { - NavigationHelper.playOnMainPlayer(context.findFragmentActivity(), queue) - } ItemList(historyItems, header = { HistoryHeader( sortKey = sortKey, onSelectSortKey = viewModel::updateOrder, - onClickClear = viewModel::deleteWatchHistory, - onClickBackground = onClickBackground, - onClickPlayAll = onClickPlayAll, - onClickPopup = onClickPopup, + queue = queue ) }) } @@ -78,11 +51,8 @@ fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { @Composable private fun HistoryHeader( sortKey: SortKey, + queue: PlayQueue, onSelectSortKey: (SortKey) -> Unit, - onClickClear: () -> Unit, - onClickBackground: () -> Unit, - onClickPlayAll: () -> Unit, - onClickPopup: () -> Unit ) { FlowRow( modifier = Modifier @@ -93,7 +63,7 @@ private fun HistoryHeader( ) { HistorySortRow(sortKey, onSelectSortKey) - HistoryButtons(onClickClear, onClickBackground, onClickPlayAll, onClickPopup) + PlaybackControlButtons(queue) } } @@ -123,75 +93,13 @@ private fun HistorySortRow( } } -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun HistoryButtons( - onClickClear: () -> Unit, - onClickBackground: () -> Unit, - onClickPlayAll: () -> Unit, - onClickPopup: () -> Unit -) { - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { - IconButtonWithLabel( - icon = Icons.Default.Headphones, - label = R.string.controls_background_title, - onClick = onClickBackground, - ) - - IconButtonWithLabel( - icon = Icons.AutoMirrored.Filled.PlaylistPlay, - label = R.string.play_all, - onClick = onClickPlayAll, - ) - - IconButtonWithLabel( - icon = Icons.Default.PictureInPicture, - label = R.string.controls_popup_title, - onClick = onClickPopup, - ) - - var openClearDialog by remember { mutableStateOf(false) } - - TextButton(onClick = { openClearDialog = true }) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(imageVector = Icons.Default.ClearAll, contentDescription = null) - Text(text = stringResource(R.string.clear)) - } - } - - if (openClearDialog) { - AlertDialog( - onDismissRequest = { openClearDialog = false }, - title = { Text(text = stringResource(R.string.delete_view_history_alert)) }, - text = { Text(text = stringResource(R.string.delete_view_history_description)) }, - confirmButton = { - TextButton(onClick = { - onClickClear() - openClearDialog = false - }) { - Text(stringResource(R.string.delete)) - } - }, - dismissButton = { - TextButton(onClick = { openClearDialog = false }) { - Text(stringResource(R.string.cancel)) - } - }, - ) - } - } -} - @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, {}, {}, {}, {}, {}) + HistoryHeader(SortKey.MOST_PLAYED, SinglePlayQueue(listOf(), 0)) {} } } } diff --git a/app/src/main/res/menu/menu_history.xml b/app/src/main/res/menu/menu_history.xml new file mode 100644 index 00000000000..a0a3e78af0a --- /dev/null +++ b/app/src/main/res/menu/menu_history.xml @@ -0,0 +1,11 @@ + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd389ad9619..40b89bd3c62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ core-ktx = "1.15.0" desugar-jdk-libs-nio = "2.0.4" documentFile = "1.0.1" exoplayer = "2.18.7" -fragment-compose = "1.8.5" +fragment-compose = "1.8.6" gradle = "8.7.3" groupie = "2.10.1" hilt = "2.51.1" From a58b0da2c1930c833d0d8fcfc02c6cb9eb1c9ae2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Tue, 25 Mar 2025 07:42:10 +0530 Subject: [PATCH 28/37] Fix actions --- .../java/org/schabi/newpipe/local/history/HistoryFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b0575a3fc74..b505331dd95 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -60,14 +60,14 @@ class HistoryFragment : Fragment() { .setNegativeButton(R.string.cancel) { dialog, which -> dialog.dismiss() } .setPositiveButton(R.string.delete) { dialog, which -> viewLifecycleOwner.lifecycleScope.launch { - launch(getExceptionHandler("Clear orphaned records")) { + launch(getExceptionHandler("Delete playback states")) { recordManager.deleteCompleteStreamStateHistory().await() Toast .makeText(context, R.string.watch_history_states_deleted, Toast.LENGTH_SHORT) .show() } - launch(getExceptionHandler("Delete search history")) { + launch(getExceptionHandler("Delete watch history")) { recordManager.deleteWholeStreamHistory().await() Toast.makeText(context, R.string.watch_history_deleted, Toast.LENGTH_SHORT) .show() From e00e2562fba03a24cb9730762cb706a1de7efa65 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 26 Mar 2025 07:57:19 +0530 Subject: [PATCH 29/37] Rename .java to .kt --- .../stream/dao/{StreamStateDAO.java => StreamStateDAO.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/database/stream/dao/{StreamStateDAO.java => StreamStateDAO.kt} (100%) 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.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java rename to app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt From fe563f3307c4be2d61b6fc0936e4f6d50fc3c196 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 26 Mar 2025 07:57:20 +0530 Subject: [PATCH 30/37] Improve DAO method calls --- .../newpipe/database/DatabaseMigrationTest.kt | 2 +- .../schabi/newpipe/database/FeedDAOTest.kt | 15 ++--- .../playlist/LocalPlaylistManagerTest.kt | 2 +- .../newpipe/database/feed/dao/FeedDAO.kt | 2 +- .../history/dao/StreamHistoryDAO.java | 18 +++--- .../newpipe/database/stream/dao/StreamDAO.kt | 5 +- .../database/stream/dao/StreamStateDAO.kt | 58 +++++-------------- .../newpipe/local/feed/FeedDatabaseManager.kt | 18 +----- .../local/feed/service/FeedLoadManager.kt | 5 +- .../local/history/HistoryRecordManager.java | 16 ++--- .../settings/HistorySettingsFragment.java | 7 ++- .../newpipe/viewmodels/StreamViewModel.kt | 3 +- 12 files changed, 53 insertions(+), 98 deletions(-) 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 cbda8eb4149..8918434e642 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 @@ -30,29 +30,29 @@ import io.reactivex.rxjava3.core.Flowable; @Dao -public abstract class StreamHistoryDAO { +public interface StreamHistoryDAO { @Insert - public abstract long insert(StreamHistoryEntity entity); + long insert(StreamHistoryEntity entity); @Delete - public abstract void delete(StreamHistoryEntity entity); + void delete(StreamHistoryEntity entity); @Query("DELETE FROM " + STREAM_HISTORY_TABLE) - public abstract Completable deleteAll(); + 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); @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM " + STREAM_TABLE @@ -74,7 +74,7 @@ public abstract class StreamHistoryDAO { + " ORDER BY " + STREAM_LATEST_DATE + " DESC" ) - public abstract PagingSource getHistoryOrderedByLastWatched(); + PagingSource getHistoryOrderedByLastWatched(); @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM " + STREAM_TABLE @@ -96,5 +96,5 @@ public abstract class StreamHistoryDAO { + " ORDER BY " + STREAM_WATCH_COUNT + " DESC" ) - public abstract PagingSource getHistoryOrderedByViewCount(); + PagingSource getHistoryOrderedByViewCount(); } 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.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt index 6f1ecf173d8..ab6bea515e4 100644 --- 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 @@ -1,49 +1,23 @@ -package org.schabi.newpipe.database.stream.dao; +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; +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 -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); +interface StreamStateDAO { + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE) + fun deleteAll(): Completable - @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - int deleteState(long streamId); + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun getState(streamId: Long): Maybe - @Insert(onConflict = OnConflictStrategy.IGNORE) - void silentInsertInternal(StreamStateEntity streamState); + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun deleteState(streamId: Long): Completable - @Transaction - default long upsert(final StreamStateEntity stream) { - silentInsertInternal(stream); - return update(stream); - } + @Upsert + fun upsert(stream: StreamStateEntity) } 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/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 29b5ceacf9f..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,19 +151,16 @@ 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 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() { @@ -278,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/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index bbfd705dd52..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"))); @@ -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/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt index fd706fb9a1f..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 @@ -25,7 +26,7 @@ class StreamViewModel(application: Application) : AndroidViewModel(application) } fun deleteStreamHistory(streamId: Long) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { historyRecordManager.deleteStreamHistoryAndState(streamId).await() } } From 6f9af2638bd95c5d7b64aad1552c36e00b3c169f Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Wed, 26 Mar 2025 07:57:30 +0530 Subject: [PATCH 31/37] Update dependencies --- gradle/libs.versions.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40b89bd3c62..b5387b1c98d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ 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.15.0" desugar-jdk-libs-nio = "2.0.4" @@ -18,12 +18,12 @@ exoplayer = "2.18.7" fragment-compose = "1.8.6" gradle = "8.7.3" groupie = "2.10.1" -hilt = "2.51.1" -jetpack-compose = "2025.01.00" +hilt = "2.55" +jetpack-compose = "2025.03.00" jsoup = "1.17.2" junit = "4.13.2" -kotlin = "2.1.0" -kotlinxCoroutinesRx3 = "1.9.0" +kotlin = "2.1.10" +kotlinxCoroutinesRx3 = "1.10.1" ktlint = "0.45.2" lazycolumnscrollbar = "2.2.0" leakcanary = "2.12" @@ -33,14 +33,14 @@ markwon = "4.6.2" material = "1.11.0" media = "1.7.0" mockitoCore = "5.6.0" -navigationCompose = "2.8.5" +navigationCompose = "2.8.9" okhttp = "4.12.0" -pagingCompose = "3.3.5" +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-rc02" # Fixes compatibility issue with newer Kotlin versions runner = "1.5.2" rxandroid = "3.0.2" rxbinding = "4.0.0" From 287db90f6277fbba29ada6fafd7c3a62e71aeeee Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Mon, 31 Mar 2025 07:15:36 +0530 Subject: [PATCH 32/37] Sort using last played by default --- .../java/org/schabi/newpipe/local/history/HistoryViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index d1a64360ce7..84f8e3b47e0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -21,7 +21,7 @@ class HistoryViewModel( ) : AndroidViewModel(application) { private val historyDao = NewPipeDatabase.getInstance(getApplication()).streamHistoryDAO() - val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.MOST_PLAYED) + val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.LAST_PLAYED) val historyItems = sortKey .flatMapLatest { Pager(PagingConfig(pageSize = 20)) { From 27a29aefc271923aebf762fa7ba87417831e1b3b Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Thu, 17 Apr 2025 07:32:41 +0530 Subject: [PATCH 33/37] Update Jetpack Compose and Room --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb1166b8e99..2ef6a0784fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ fragment-compose = "1.8.6" gradle = "8.7.3" groupie = "2.10.1" hilt = "2.55" -jetpack-compose = "2025.03.00" +jetpack-compose = "2025.04.00" jsoup = "1.17.2" junit = "4.13.2" kotlin = "2.1.10" @@ -40,7 +40,7 @@ preference = "1.2.1" prettytime = "5.0.8.Final" processPhoenix = "2.1.2" recyclerview = "1.3.2" -room = "2.7.0-rc02" # Fixes compatibility issue with newer Kotlin versions +room = "2.7.0" # Fixes compatibility issue with newer Kotlin versions runner = "1.5.2" rxandroid = "3.0.2" rxbinding = "4.0.0" From 4eae345057819d9237bdb1a95911d3950f6ce525 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Fri, 11 Jul 2025 22:26:14 +0530 Subject: [PATCH 34/37] Combine history ordered query methods into one --- .../history/dao/StreamHistoryDAO.java | 75 ++++++++----------- .../newpipe/local/history/HistoryViewModel.kt | 5 +- 2 files changed, 32 insertions(+), 48 deletions(-) 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 57058426968..5f271655f01 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 @@ -18,11 +18,15 @@ import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; +import androidx.room.RawQuery; +import androidx.sqlite.db.SimpleSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteQuery; import org.schabi.newpipe.database.history.model.StreamHistoryEntity; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.model.StreamEntity; +import org.schabi.newpipe.local.history.SortKey; import java.util.List; @@ -31,6 +35,20 @@ @Dao public interface StreamHistoryDAO { + String ORDERED_HISTORY_QUERY = "SELECT * FROM " + STREAM_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; + @Insert long insert(StreamHistoryEntity entity); @@ -60,47 +78,16 @@ public interface StreamHistoryDAO { + " 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 + ")" - - + " 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_WATCH_COUNT + " DESC" - ) - PagingSource getHistoryOrderedByViewCount(); + @RawQuery(observedEntities = {StreamStatisticsEntry.class, StreamEntity.class, + StreamHistoryEntity.class}) + PagingSource getOrderedHistoryByRaw(SupportSQLiteQuery query); + + default PagingSource getOrderedHistory(SortKey key) { + final String orderBy = switch (key) { + case LAST_PLAYED -> STREAM_LATEST_DATE; + case MOST_PLAYED -> STREAM_WATCH_COUNT; + }; + return getOrderedHistoryByRaw(new SimpleSQLiteQuery(ORDERED_HISTORY_QUERY + " ORDER BY " + + orderBy + " DESC")); + } } 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 index 84f8e3b47e0..3240e2afb46 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -25,10 +25,7 @@ class HistoryViewModel( val historyItems = sortKey .flatMapLatest { Pager(PagingConfig(pageSize = 20)) { - when (it) { - SortKey.LAST_PLAYED -> historyDao.getHistoryOrderedByLastWatched() - SortKey.MOST_PLAYED -> historyDao.getHistoryOrderedByViewCount() - } + historyDao.getOrderedHistory(it) }.flow } .map { pagingData -> pagingData.map { Stream(it) } } From f89ee3b3761b7b55652abd9db0a1b14b73585fa2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 12 Jul 2025 05:59:38 +0530 Subject: [PATCH 35/37] Rename .java to .kt --- ...toreSettingsFragment.java => BackupRestoreSettingsFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/schabi/newpipe/settings/{BackupRestoreSettingsFragment.java => BackupRestoreSettingsFragment.kt} (100%) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt similarity index 100% rename from app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java rename to app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt From eac0dcd70d62b3aa322a4af7693427336cfcece2 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 12 Jul 2025 05:59:38 +0530 Subject: [PATCH 36/37] Run database checkpoint on IO coroutine dispatcher --- .../settings/BackupRestoreSettingsFragment.kt | 413 ++++++++---------- 1 file changed, 192 insertions(+), 221 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt index fca158c28e5..8ea78e0e20e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt @@ -1,229 +1,196 @@ -package org.schabi.newpipe.settings; - -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.widget.Toast; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; - -import com.grack.nanojson.JsonParserException; - -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.R; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.settings.export.BackupFileLocator; -import org.schabi.newpipe.settings.export.ImportExportManager; -import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; -import org.schabi.newpipe.streams.io.StoredFileHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ZipHelper; - -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -public class BackupRestoreSettingsFragment extends BasePreferenceFragment { - - private static final String ZIP_MIME_TYPE = "application/zip"; - - private final SimpleDateFormat exportDateFormat = - new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - private ImportExportManager manager; - private String importExportDataPathKey; - private final ActivityResultLauncher requestImportPathLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - this::requestImportPathResult); - private final ActivityResultLauncher requestExportPathLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - this::requestExportPathResult); - - - @Override - public void onCreatePreferences(@Nullable final Bundle savedInstanceState, - @Nullable final String rootKey) { - final var dbDir = requireContext().getDatabasePath(BackupFileLocator.FILE_NAME_DB).toPath() - .getParent(); - manager = new ImportExportManager(new BackupFileLocator(dbDir)); - - importExportDataPathKey = getString(R.string.import_export_data_path); - - addPreferencesFromResourceRegistry(); - - final Preference importDataPreference = requirePreference(R.string.import_data); - importDataPreference.setOnPreferenceClickListener((Preference p) -> { - NoFileManagerSafeGuard.launchSafe( - requestImportPathLauncher, - StoredFileHelper.getPicker(requireContext(), - ZIP_MIME_TYPE, getImportExportDataUri()), - TAG, - getContext() - ); - - return true; - }); - - final Preference exportDataPreference = requirePreference(R.string.export_data); - exportDataPreference.setOnPreferenceClickListener((final Preference p) -> { - NoFileManagerSafeGuard.launchSafe( - requestExportPathLauncher, - StoredFileHelper.getNewPicker(requireContext(), - "NewPipeData-" + exportDateFormat.format(new Date()) + ".zip", - ZIP_MIME_TYPE, getImportExportDataUri()), - TAG, - getContext() - ); +package org.schabi.newpipe.settings + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.appcompat.app.AlertDialog +import androidx.core.content.edit +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import com.grack.nanojson.JsonParserException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.R +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification +import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar +import org.schabi.newpipe.error.UserAction +import org.schabi.newpipe.settings.export.BackupFileLocator +import org.schabi.newpipe.settings.export.ImportExportManager +import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard +import org.schabi.newpipe.streams.io.StoredFileHelper +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.ZipHelper +import java.io.IOException +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.Locale + +class BackupRestoreSettingsFragment : BasePreferenceFragment() { + private val exportDateFormat = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss", Locale.US) + private lateinit var manager: ImportExportManager + private val requestImportPathLauncher = + registerForActivityResult(StartActivityForResult(), this::requestImportPathResult) + private val requestExportPathLauncher = + registerForActivityResult(StartActivityForResult(), this::requestExportPathResult) + + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + val activity = requireActivity() + val dbDir = activity.getDatabasePath(BackupFileLocator.FILE_NAME_DB).toPath().parent + manager = ImportExportManager(BackupFileLocator(dbDir)) + + addPreferencesFromResourceRegistry() + + val importDataPreference = requirePreference(R.string.import_data) + importDataPreference.setOnPreferenceClickListener { + val picker = StoredFileHelper.getPicker(activity, ZIP_MIME_TYPE, importExportDataUri) + NoFileManagerSafeGuard.launchSafe(requestImportPathLauncher, picker, TAG, activity) + true + } - return true; - }); + val exportDataPreference = requirePreference(R.string.export_data) + exportDataPreference.setOnPreferenceClickListener { + val filename = "NewPipeData-${exportDateFormat.format(LocalDateTime.now())}.zip" + val picker = StoredFileHelper.getNewPicker(activity, filename, ZIP_MIME_TYPE, importExportDataUri) + NoFileManagerSafeGuard.launchSafe(requestExportPathLauncher, picker, TAG, activity) + true + } - final Preference resetSettings = findPreference(getString(R.string.reset_settings)); - // Resets all settings by deleting shared preference and restarting the app - // A dialogue will pop up to confirm if user intends to reset all settings - assert resetSettings != null; - resetSettings.setOnPreferenceClickListener(preference -> { + val resetSettings = requirePreference(R.string.reset_settings) + resetSettings.onPreferenceClickListener = Preference.OnPreferenceClickListener { // Show Alert Dialogue - final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); - builder.setMessage(R.string.reset_all_settings); - builder.setCancelable(true); - builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - // Deletes all shared preferences xml files. - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()); - sharedPreferences.edit().clear().apply(); - // Restarts the app - if (getActivity() == null) { - return; - } - NavigationHelper.restartApp(getActivity()); - }); - builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> { - }); - final AlertDialog alertDialog = builder.create(); - alertDialog.show(); - return true; - }); + AlertDialog + .Builder(activity) + .setMessage(R.string.reset_all_settings) + .setCancelable(true) + .setPositiveButton(R.string.ok) { _, _ -> + // Deletes all shared preferences xml files. + defaultPreferences.edit { clear() } + NavigationHelper.restartApp(activity) + }.setNegativeButton(R.string.cancel) { _, _ -> } + .show() + true + } } - private void requestExportPathResult(final ActivityResult result) { - assureCorrectAppLanguage(requireContext()); - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // will be saved only on success - final Uri lastExportDataUri = result.getData().getData(); - - final StoredFileHelper file = new StoredFileHelper( - requireContext(), result.getData().getData(), ZIP_MIME_TYPE); + private fun requestExportPathResult(result: ActivityResult) { + val context = requireContext() + val lastExportDataUri = result.data?.data - exportDatabase(file, lastExportDataUri); + Localization.assureCorrectAppLanguage(context) + if (result.resultCode == Activity.RESULT_OK && lastExportDataUri != null) { + // will be saved only on success + val file = StoredFileHelper(context, lastExportDataUri, ZIP_MIME_TYPE) + exportDatabase(file, lastExportDataUri) } } - private void requestImportPathResult(final ActivityResult result) { - assureCorrectAppLanguage(requireContext()); - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // will be saved only on success - final Uri lastImportDataUri = result.getData().getData(); + private fun requestImportPathResult(result: ActivityResult) { + val context = requireContext() + val lastImportDataUri = result.data?.data - final StoredFileHelper file = new StoredFileHelper( - requireContext(), result.getData().getData(), ZIP_MIME_TYPE); + Localization.assureCorrectAppLanguage(context) + if (result.resultCode == Activity.RESULT_OK && lastImportDataUri != null) { + // will be saved only on success + val file = StoredFileHelper(context, lastImportDataUri, ZIP_MIME_TYPE) - new androidx.appcompat.app.AlertDialog.Builder(requireActivity()) - .setMessage(R.string.override_current_data) - .setPositiveButton(R.string.ok, (d, id) -> - importDatabase(file, lastImportDataUri)) - .setNegativeButton(R.string.cancel, (d, id) -> - d.cancel()) - .show(); + AlertDialog.Builder(context) + .setMessage(R.string.override_current_data) + .setPositiveButton(R.string.ok) { _, _ -> importDatabase(file, lastImportDataUri) } + .setNegativeButton(R.string.cancel) { dialog, id -> dialog.cancel() } + .show() } } - private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) { - try { - //checkpoint before export - NewPipeDatabase.checkpoint(); + private fun exportDatabase(file: StoredFileHelper, exportDataUri: Uri) { + lifecycleScope.launch( + CoroutineExceptionHandler { context, throwable -> + showErrorSnackbar(throwable, "Exporting database and settings") + } + ) { + // checkpoint before export + withContext(Dispatchers.IO) { + NewPipeDatabase.checkpoint() + } - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(requireContext()); - manager.exportDatabase(preferences, file); + manager.exportDatabase(defaultPreferences, file) - saveLastImportExportDataUri(exportDataUri); // save export path only on success + saveLastImportExportDataUri(exportDataUri) // save export path only on success Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) - .show(); - } catch (final Exception e) { - showErrorSnackbar(e, "Exporting database and settings"); + .show() } } - private void importDatabase(final StoredFileHelper file, final Uri importDataUri) { + private fun importDatabase(file: StoredFileHelper, importDataUri: Uri) { + val context = requireContext() + // check if file is supported if (!ZipHelper.isValidZipFile(file)) { - Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); - return; + Toast.makeText(context, R.string.no_valid_zip_file, Toast.LENGTH_SHORT) + .show() + return } try { - manager.ensureDbDirectoryExists(); + manager.ensureDbDirectoryExists() // replace the current database if (!manager.extractDb(file)) { - Toast.makeText(requireContext(), R.string.could_not_import_all_files, - Toast.LENGTH_LONG) - .show(); + Toast.makeText(context, R.string.could_not_import_all_files, Toast.LENGTH_LONG) + .show() } // if settings file exist, ask if it should be imported. - final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file); + val hasJsonPrefs = manager.exportHasJsonPrefs(file) if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) { - new androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle(R.string.import_settings) - .setMessage(hasJsonPrefs ? null : requireContext() - .getString(R.string.import_settings_vulnerable_format)) - .setOnDismissListener(dialog -> finishImport(importDataUri)) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - dialog.dismiss(); - finishImport(importDataUri); - }) - .setPositiveButton(R.string.ok, (dialog, which) -> { - dialog.dismiss(); - final Context context = requireContext(); - final SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - try { - if (hasJsonPrefs) { - manager.loadJsonPrefs(file, prefs); - } else { - manager.loadSerializedPrefs(file, prefs); - } - } catch (IOException | ClassNotFoundException | JsonParserException e) { - createErrorNotification(e, "Importing preferences"); - return; + AlertDialog.Builder(context) + .setTitle(R.string.import_settings) + .setMessage(if (hasJsonPrefs) null else getString(R.string.import_settings_vulnerable_format)) + .setOnDismissListener { finishImport(importDataUri) } + .setNegativeButton(R.string.cancel) { dialog, which -> + dialog.dismiss() + finishImport(importDataUri) + } + .setPositiveButton(R.string.ok) { dialog, which -> + dialog.dismiss() + try { + if (hasJsonPrefs) { + manager.loadJsonPrefs(file, defaultPreferences) + } else { + manager.loadSerializedPrefs(file, defaultPreferences) } - cleanImport(context, prefs); - finishImport(importDataUri); - }) - .show(); + } catch (e: IOException) { + createErrorNotification(e, "Importing preferences") + return@setPositiveButton + } catch (e: ClassNotFoundException) { + createErrorNotification(e, "Importing preferences") + return@setPositiveButton + } catch (e: JsonParserException) { + createErrorNotification(e, "Importing preferences") + return@setPositiveButton + } + cleanImport(requireContext(), defaultPreferences) + finishImport(importDataUri) + } + .show() } else { - finishImport(importDataUri); + finishImport(importDataUri) } - } catch (final Exception e) { - showErrorSnackbar(e, "Importing database and settings"); + } catch (e: Exception) { + showErrorSnackbar(e, "Importing database and settings") } } @@ -233,13 +200,14 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { * @param context the context used for the import * @param prefs the preferences used while running the import */ - private void cleanImport(@NonNull final Context context, - @NonNull final SharedPreferences prefs) { + private fun cleanImport( + context: Context, + prefs: SharedPreferences + ) { // Check if media tunnelling needs to be disabled automatically, // if it was disabled automatically in the imported preferences. - final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key); - final String automaticTunnelingKey = - context.getString(R.string.disabled_media_tunneling_automatically_key); + val tunnelingKey = getString(R.string.disable_media_tunneling_key) + val automaticTunnelingKey = getString(R.string.disabled_media_tunneling_automatically_key) // R.string.disable_media_tunneling_key should always be true // if R.string.disabled_media_tunneling_automatically_key equals 1, // but we double check here just to be sure and to avoid regressions @@ -247,15 +215,15 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { // R.string.disabled_media_tunneling_automatically_key == 0: // automatic value overridden by user in settings // R.string.disabled_media_tunneling_automatically_key == -1: not set - final boolean wasMediaTunnelingDisabledAutomatically = - prefs.getInt(automaticTunnelingKey, -1) == 1 - && prefs.getBoolean(tunnelingKey, false); + val wasMediaTunnelingDisabledAutomatically = + prefs.getInt(automaticTunnelingKey, -1) == 1 && + prefs.getBoolean(tunnelingKey, false) if (wasMediaTunnelingDisabledAutomatically) { - prefs.edit() - .putInt(automaticTunnelingKey, -1) - .putBoolean(tunnelingKey, false) - .apply(); - NewPipeSettings.setMediaTunneling(context); + prefs.edit { + putInt(automaticTunnelingKey, -1) + putBoolean(tunnelingKey, false) + } + NewPipeSettings.setMediaTunneling(context) } } @@ -264,32 +232,35 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { * * @param importDataUri The import path to save */ - private void finishImport(final Uri importDataUri) { + private fun finishImport(importDataUri: Uri) { // save import path only on success - saveLastImportExportDataUri(importDataUri); + saveLastImportExportDataUri(importDataUri) // restart app to properly load db - NavigationHelper.restartApp(requireActivity()); + NavigationHelper.restartApp(requireActivity()) } - private Uri getImportExportDataUri() { - final String path = defaultPreferences.getString(importExportDataPathKey, null); - return isBlank(path) ? null : Uri.parse(path); + private val importExportDataUri: Uri? + get() = defaultPreferences + .getString(getString(R.string.import_export_data_path), null)?.toUri() + + private fun saveLastImportExportDataUri(importExportDataUri: Uri) { + defaultPreferences.edit { + putString(getString(R.string.import_export_data_path), importExportDataUri.toString()) + } } - private void saveLastImportExportDataUri(final Uri importExportDataUri) { - final SharedPreferences.Editor editor = defaultPreferences.edit() - .putString(importExportDataPathKey, importExportDataUri.toString()); - editor.apply(); + private fun showErrorSnackbar(e: Throwable, request: String) { + showSnackbar(this, ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)) } - private void showErrorSnackbar(final Throwable e, final String request) { - ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)); + private fun createErrorNotification(e: Throwable, request: String) { + createNotification( + requireContext(), + ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request) + ) } - private void createErrorNotification(final Throwable e, final String request) { - ErrorUtil.createNotification( - requireContext(), - new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request) - ); + companion object { + private const val ZIP_MIME_TYPE = "application/zip" } } From 8b2c61432e576badd28529d7bda4bbd9f575a3ff Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 12 Jul 2025 06:14:42 +0530 Subject: [PATCH 37/37] Cleanup --- .../newpipe/settings/BackupRestoreSettingsFragment.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt index 8ea78e0e20e..cb070ba19cc 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt @@ -12,7 +12,6 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope -import androidx.preference.Preference import com.grack.nanojson.JsonParserException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers @@ -54,23 +53,20 @@ class BackupRestoreSettingsFragment : BasePreferenceFragment() { addPreferencesFromResourceRegistry() - val importDataPreference = requirePreference(R.string.import_data) - importDataPreference.setOnPreferenceClickListener { + requirePreference(R.string.import_data).setOnPreferenceClickListener { val picker = StoredFileHelper.getPicker(activity, ZIP_MIME_TYPE, importExportDataUri) NoFileManagerSafeGuard.launchSafe(requestImportPathLauncher, picker, TAG, activity) true } - val exportDataPreference = requirePreference(R.string.export_data) - exportDataPreference.setOnPreferenceClickListener { + requirePreference(R.string.export_data).setOnPreferenceClickListener { val filename = "NewPipeData-${exportDateFormat.format(LocalDateTime.now())}.zip" val picker = StoredFileHelper.getNewPicker(activity, filename, ZIP_MIME_TYPE, importExportDataUri) NoFileManagerSafeGuard.launchSafe(requestExportPathLauncher, picker, TAG, activity) true } - val resetSettings = requirePreference(R.string.reset_settings) - resetSettings.onPreferenceClickListener = Preference.OnPreferenceClickListener { + requirePreference(R.string.reset_settings).setOnPreferenceClickListener { // Show Alert Dialogue AlertDialog .Builder(activity)