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 a61caad0676..8ce35be606e 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/FeedDAOTest.kt @@ -3,6 +3,9 @@ package org.schabi.newpipe.database import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx3.await import io.reactivex.rxjava3.core.Single import java.io.IOException import java.time.OffsetDateTime @@ -22,6 +25,8 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.stream.StreamType +import java.io.IOException +import java.time.OffsetDateTime class FeedDAOTest { private lateinit var db: AppDatabase @@ -106,14 +111,10 @@ class FeedDAOTest { ) } - private fun setupUnlinkDelete(time: String) { + private fun setupUnlinkDelete(time: String) = runBlocking(Dispatchers.IO) { clearAndFillTables() - Single.fromCallable { - feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time)) - }.blockingSubscribe() - Single.fromCallable { - streamDAO.deleteOrphans() - }.blockingSubscribe() + feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time)) + streamDAO.deleteOrphans().await() } private fun clearAndFillTables() { 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 5861fa767f1..245a0cfb957 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/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt index efc51e1a434..c44e5e9cc24 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 @@ -122,7 +119,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/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index b33619dea7a..b129d18a1fc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -22,9 +22,6 @@ import org.schabi.newpipe.local.holder.LocalPlaylistStreamCardItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistStreamGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder; -import org.schabi.newpipe.local.holder.LocalStatisticStreamCardItemHolder; -import org.schabi.newpipe.local.holder.LocalStatisticStreamGridItemHolder; -import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder; import org.schabi.newpipe.local.holder.RemoteBookmarkPlaylistItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistCardItemHolder; import org.schabi.newpipe.local.holder.RemotePlaylistGridItemHolder; @@ -65,10 +62,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter new HeaderFooterHolder(header); + case FOOTER_TYPE -> new HeaderFooterHolder(footer); + case LOCAL_PLAYLIST_HOLDER_TYPE -> + new LocalPlaylistItemHolder(localItemBuilder, parent); + case LOCAL_PLAYLIST_GRID_HOLDER_TYPE -> + new LocalPlaylistGridItemHolder(localItemBuilder, parent); + case LOCAL_PLAYLIST_CARD_HOLDER_TYPE -> + new LocalPlaylistCardItemHolder(localItemBuilder, parent); + case LOCAL_BOOKMARK_PLAYLIST_HOLDER_TYPE -> + new LocalBookmarkPlaylistItemHolder(localItemBuilder, parent); + case REMOTE_PLAYLIST_HOLDER_TYPE -> + new RemotePlaylistItemHolder(localItemBuilder, parent); + case REMOTE_PLAYLIST_GRID_HOLDER_TYPE -> + new RemotePlaylistGridItemHolder(localItemBuilder, parent); + case REMOTE_PLAYLIST_CARD_HOLDER_TYPE -> + new RemotePlaylistCardItemHolder(localItemBuilder, parent); + case REMOTE_BOOKMARK_PLAYLIST_HOLDER_TYPE -> + new RemoteBookmarkPlaylistItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_HOLDER_TYPE -> + new LocalPlaylistStreamItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_GRID_HOLDER_TYPE -> + new LocalPlaylistStreamGridItemHolder(localItemBuilder, parent); + case STREAM_PLAYLIST_CARD_HOLDER_TYPE -> + new LocalPlaylistStreamCardItemHolder(localItemBuilder, parent); + default -> { Log.e(TAG, "No view type has been considered for holder: [" + type + "]"); - return new FallbackViewHolder(new View(parent.getContext())); - } + yield new FallbackViewHolder(new View(parent.getContext())); + } + }; } @SuppressWarnings("FinalParameters") diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index 3e3a47f57e4..dbae1ed5e67 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,7 +1,6 @@ package org.schabi.newpipe.local.feed import android.content.Context -import android.util.Log import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable @@ -11,6 +10,7 @@ import java.time.LocalDate import java.time.OffsetDateTime import java.time.ZoneOffset import org.schabi.newpipe.MainActivity.DEBUG +import kotlinx.coroutines.rx3.await import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity @@ -110,20 +110,9 @@ class FeedDatabaseManager(context: Context) { ) } - fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) { + suspend fun removeOrphansOrOlderStreams(oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE) { feedTable.unlinkStreamsOlderThan(oldestAllowedDate) - streamTable.deleteOrphans() - } - - fun clear() { - feedTable.deleteAll() - val deletedOrphans = streamTable.deleteOrphans() - if (DEBUG) { - Log.d( - this::class.java.simpleName, - "clear() → streamTable.deleteOrphans() → $deletedOrphans" - ) - } + streamTable.deleteOrphans().await() } // ///////////////////////////////////////////////////////////////////////// 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 3090a92d4c2..d18ee5c4dc6 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,7 +4,6 @@ import android.content.Context import android.content.SharedPreferences import androidx.preference.PreferenceManager import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Notification import io.reactivex.rxjava3.core.Single @@ -15,6 +14,8 @@ import java.time.OffsetDateTime import java.time.ZoneOffset import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.rx3.rxCompletable import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.subscription.NotificationMode @@ -261,7 +262,7 @@ class FeedLoadManager(private val context: Context) { * Remove streams from the feed which are older than [FeedDatabaseManager.FEED_OLDEST_ALLOWED_DATE]. * Remove streams from the database which are not linked / used by any table. */ - private fun postProcessFeed() = Completable.fromRunnable { + private fun postProcessFeed() = rxCompletable(Dispatchers.IO) { FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(R.string.feed_processing_message)) feedDatabaseManager.removeOrphansOrOlderStreams() diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt new file mode 100644 index 00000000000..b505331dd95 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryFragment.kt @@ -0,0 +1,94 @@ +package org.schabi.newpipe.local.history + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material3.Surface +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.compose.content +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await +import org.schabi.newpipe.R +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.ErrorUtil.Companion.openActivity +import org.schabi.newpipe.error.UserAction +import org.schabi.newpipe.ui.screens.HistoryScreen +import org.schabi.newpipe.ui.theme.AppTheme + +class HistoryFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = content { + AppTheme { + Surface { + HistoryScreen() + } + } + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + val context = requireActivity() + (context as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.title_activity_history) + + val recordManager = HistoryRecordManager(context) + context.addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_history, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.action_history_clear -> { + AlertDialog.Builder(context) + .setTitle(R.string.delete_view_history_alert) + .setNegativeButton(R.string.cancel) { dialog, which -> dialog.dismiss() } + .setPositiveButton(R.string.delete) { dialog, which -> + viewLifecycleOwner.lifecycleScope.launch { + launch(getExceptionHandler("Delete playback states")) { + recordManager.deleteCompleteStreamStateHistory().await() + Toast + .makeText(context, R.string.watch_history_states_deleted, Toast.LENGTH_SHORT) + .show() + } + + launch(getExceptionHandler("Delete watch history")) { + recordManager.deleteWholeStreamHistory().await() + Toast.makeText(context, R.string.watch_history_deleted, Toast.LENGTH_SHORT) + .show() + } + + launch(getExceptionHandler("Clear orphaned records")) { + recordManager.removeOrphanedRecords().await() + } + } + } + .show() + } + } + return true + } + }, + viewLifecycleOwner + ) + } + + private fun getExceptionHandler(action: String) = CoroutineExceptionHandler { _, throwable -> + openActivity(requireContext(), ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, action)) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index f2fdf9eba63..09cc193b5b9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -151,30 +151,22 @@ public Maybe onViewed(final StreamInfo info) { } public Completable deleteStreamHistoryAndState(final long streamId) { - return Completable.fromAction(() -> { - streamStateTable.deleteState(streamId); - streamHistoryTable.deleteStreamHistory(streamId); - }).subscribeOn(Schedulers.io()); + return streamStateTable.deleteState(streamId) + .andThen(streamHistoryTable.deleteStreamHistory(streamId)); } - public Single deleteWholeStreamHistory() { - return Single.fromCallable(streamHistoryTable::deleteAll) - .subscribeOn(Schedulers.io()); + public Completable deleteWholeStreamHistory() { + return streamHistoryTable.deleteAll().subscribeOn(Schedulers.io()); } - public Single deleteCompleteStreamStateHistory() { - return Single.fromCallable(streamStateTable::deleteAll) - .subscribeOn(Schedulers.io()); + public Completable deleteCompleteStreamStateHistory() { + return streamStateTable.deleteAll().subscribeOn(Schedulers.io()); } public Flowable> getStreamHistorySortedById() { return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); } - public Flowable> getStreamStatistics() { - return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); - } - private boolean isStreamHistoryEnabled() { return sharedPreferences.getBoolean(streamHistoryKey, false); } @@ -283,8 +275,7 @@ public Single> loadLocalStreamStateBatch( // Utility /////////////////////////////////////////////////////// - public Single removeOrphanedRecords() { - return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io()); + public Completable removeOrphanedRecords() { + return streamTable.deleteOrphans().subscribeOn(Schedulers.io()); } - } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt new file mode 100644 index 00000000000..3240e2afb46 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryViewModel.kt @@ -0,0 +1,42 @@ +package org.schabi.newpipe.local.history + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.map +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.ui.components.items.Stream + +class HistoryViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle, +) : AndroidViewModel(application) { + private val historyDao = NewPipeDatabase.getInstance(getApplication()).streamHistoryDAO() + + val sortKey = savedStateHandle.getStateFlow(ORDER_KEY, SortKey.LAST_PLAYED) + val historyItems = sortKey + .flatMapLatest { + Pager(PagingConfig(pageSize = 20)) { + historyDao.getOrderedHistory(it) + }.flow + } + .map { pagingData -> pagingData.map { Stream(it) } } + .flowOn(Dispatchers.IO) + .cachedIn(viewModelScope) + + fun updateOrder(sortKey: SortKey) { + savedStateHandle[ORDER_KEY] = sortKey + } + + companion object { + private const val ORDER_KEY = "order" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt new file mode 100644 index 00000000000..b4bb6645dd6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/history/SortKey.kt @@ -0,0 +1,9 @@ +package org.schabi.newpipe.local.history + +import androidx.annotation.StringRes +import org.schabi.newpipe.R + +enum class SortKey(@StringRes val title: Int) { + LAST_PLAYED(R.string.history_sort_date), + MOST_PLAYED(R.string.history_sort_views) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java deleted file mode 100644 index 3302e387ec5..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ /dev/null @@ -1,392 +0,0 @@ -package org.schabi.newpipe.local.history; - -import android.content.Context; -import android.os.Bundle; -import android.os.Parcelable; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.viewbinding.ViewBinding; - -import com.evernote.android.state.State; -import com.google.android.material.snackbar.Snackbar; - -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.databinding.PlaylistControlBinding; -import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; -import org.schabi.newpipe.info_list.dialog.InfoItemDialog; -import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; -import org.schabi.newpipe.local.BaseLocalListFragment; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.settings.HistorySettingsFragment; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.OnClickGesture; -import org.schabi.newpipe.util.PlayButtonHelper; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; - -public class StatisticsPlaylistFragment - extends BaseLocalListFragment, Void> - implements PlaylistControlViewHolder { - private final CompositeDisposable disposables = new CompositeDisposable(); - @State - Parcelable itemsListState; - private StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED; - - private StatisticPlaylistControlBinding headerBinding; - private PlaylistControlBinding playlistControlBinding; - - /* Used for independent events */ - private Subscription databaseSubscription; - private HistoryRecordManager recordManager; - - private List processResult(final List results) { - final Comparator comparator; - switch (sortMode) { - case LAST_PLAYED: - comparator = Comparator.comparing(StreamStatisticsEntry::getLatestAccessDate); - break; - case MOST_PLAYED: - comparator = Comparator.comparingLong(StreamStatisticsEntry::getWatchCount); - break; - default: - return null; - } - Collections.sort(results, comparator.reversed()); - return results; - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Creation - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - recordManager = new HistoryRecordManager(getContext()); - } - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_playlist, container, false); - } - - @Override - public void onResume() { - super.onResume(); - if (activity != null) { - setTitle(activity.getString(R.string.title_activity_history)); - } - } - - @Override - public void onCreateOptionsMenu(@NonNull final Menu menu, - @NonNull final MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_history, menu); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Views - /////////////////////////////////////////////////////////////////////////// - - @Override - protected void initViews(final View rootView, final Bundle savedInstanceState) { - super.initViews(rootView, savedInstanceState); - if (!useAsFrontPage) { - setTitle(getString(R.string.title_last_played)); - } - } - - @Override - protected ViewBinding getListHeader() { - headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(), - itemsList, false); - playlistControlBinding = headerBinding.playlistControl; - - return headerBinding; - } - - @Override - protected void initListeners() { - super.initListeners(); - - itemListAdapter.setSelectedListener(new OnClickGesture<>() { - @Override - public void selected(final LocalItem selectedItem) { - if (selectedItem instanceof StreamStatisticsEntry) { - final StreamEntity item = - ((StreamStatisticsEntry) selectedItem).getStreamEntity(); - NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), - item.getServiceId(), item.getUrl(), item.getTitle(), null, false); - } - } - - @Override - public void held(final LocalItem selectedItem) { - if (selectedItem instanceof StreamStatisticsEntry) { - showInfoItemDialog((StreamStatisticsEntry) selectedItem); - } - } - }); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_history_clear) { - HistorySettingsFragment - .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); - } else { - return super.onOptionsItemSelected(item); - } - return true; - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Loading - /////////////////////////////////////////////////////////////////////////// - - @Override - public void startLoading(final boolean forceLoad) { - super.startLoading(forceLoad); - recordManager.getStreamStatistics() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(getHistoryObserver()); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment LifeCycle - Destruction - /////////////////////////////////////////////////////////////////////////// - - @Override - public void onPause() { - super.onPause(); - itemsListState = Objects.requireNonNull(itemsList.getLayoutManager()).onSaveInstanceState(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - - if (itemListAdapter != null) { - itemListAdapter.unsetSelectedListener(); - } - - headerBinding = null; - playlistControlBinding = null; - - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - databaseSubscription = null; - } - - @Override - public void onDestroy() { - super.onDestroy(); - recordManager = null; - itemsListState = null; - } - - /////////////////////////////////////////////////////////////////////////// - // Statistics Loader - /////////////////////////////////////////////////////////////////////////// - - private Subscriber> getHistoryObserver() { - return new Subscriber>() { - @Override - public void onSubscribe(final Subscription s) { - showLoading(); - - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - databaseSubscription = s; - databaseSubscription.request(1); - } - - @Override - public void onNext(final List streams) { - handleResult(streams); - if (databaseSubscription != null) { - databaseSubscription.request(1); - } - } - - @Override - public void onError(final Throwable exception) { - showError( - new ErrorInfo(exception, UserAction.SOMETHING_ELSE, "History Statistics")); - } - - @Override - public void onComplete() { - } - }; - } - - @Override - public void handleResult(@NonNull final List result) { - super.handleResult(result); - if (itemListAdapter == null) { - return; - } - - playlistControlBinding.getRoot().setVisibility(View.VISIBLE); - - itemListAdapter.clearStreamItemList(); - - if (result.isEmpty()) { - showEmptyState(); - return; - } - - itemListAdapter.addItems(processResult(result)); - if (itemsListState != null && itemsList.getLayoutManager() != null) { - itemsList.getLayoutManager().onRestoreInstanceState(itemsListState); - itemsListState = null; - } - - PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this); - - headerBinding.sortButton.setOnClickListener(view -> toggleSortMode()); - - hideLoading(); - } - - /////////////////////////////////////////////////////////////////////////// - // Fragment Error Handling - /////////////////////////////////////////////////////////////////////////// - - @Override - protected void resetFragment() { - super.resetFragment(); - if (databaseSubscription != null) { - databaseSubscription.cancel(); - } - } - - /*////////////////////////////////////////////////////////////////////////// - // Utils - //////////////////////////////////////////////////////////////////////////*/ - - private void toggleSortMode() { - if (sortMode == StatisticSortMode.LAST_PLAYED) { - sortMode = StatisticSortMode.MOST_PLAYED; - setTitle(getString(R.string.title_most_played)); - headerBinding.sortButtonIcon.setImageResource(R.drawable.ic_history); - headerBinding.sortButtonText.setText(R.string.title_last_played); - } else { - sortMode = StatisticSortMode.LAST_PLAYED; - setTitle(getString(R.string.title_last_played)); - headerBinding.sortButtonIcon.setImageResource( - R.drawable.ic_filter_list); - headerBinding.sortButtonText.setText(R.string.title_most_played); - } - startLoading(true); - } - - private PlayQueue getPlayQueueStartingAt(final StreamStatisticsEntry infoItem) { - return getPlayQueue(Math.max(itemListAdapter.getItemsList().indexOf(infoItem), 0)); - } - - private void showInfoItemDialog(final StreamStatisticsEntry item) { - final Context context = getContext(); - final StreamInfoItem infoItem = item.toStreamInfoItem(); - - try { - final InfoItemDialog.Builder dialogBuilder = - new InfoItemDialog.Builder(getActivity(), context, this, infoItem); - - // set entries in the middle; the others are added automatically - dialogBuilder - .addEntry(StreamDialogDefaultEntry.DELETE) - .setAction( - StreamDialogDefaultEntry.DELETE, - (f, i) -> deleteEntry( - Math.max(itemListAdapter.getItemsList().indexOf(item), 0))) - .create() - .show(); - } catch (final IllegalArgumentException e) { - InfoItemDialog.Builder.reportErrorDuringInitialization(e, infoItem); - } - } - - private void deleteEntry(final int index) { - final LocalItem infoItem = itemListAdapter.getItemsList().get(index); - if (infoItem instanceof StreamStatisticsEntry) { - final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem; - final Disposable onDelete = recordManager - .deleteStreamHistoryAndState(entry.getStreamId()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - if (getView() != null) { - Snackbar.make(getView(), R.string.one_item_deleted, - Snackbar.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), - R.string.one_item_deleted, - Toast.LENGTH_SHORT).show(); - } - }, - throwable -> showSnackBarError(new ErrorInfo(throwable, - UserAction.DELETE_FROM_HISTORY, "Deleting item"))); - - disposables.add(onDelete); - } - } - - @Override - public PlayQueue getPlayQueue() { - return getPlayQueue(0); - } - - private PlayQueue getPlayQueue(final int index) { - if (itemListAdapter == null) { - return new SinglePlayQueue(Collections.emptyList(), 0); - } - - final List infoItems = itemListAdapter.getItemsList(); - final List streamInfoItems = new ArrayList<>(infoItems.size()); - for (final LocalItem item : infoItems) { - if (item instanceof StreamStatisticsEntry) { - streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem()); - } - } - return new SinglePlayQueue(streamInfoItems, index); - } - - private enum StatisticSortMode { - LAST_PLAYED, - MOST_PLAYED, - } -} - diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java deleted file mode 100644 index 4e03d5fb105..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamCardItemHolder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.ViewGroup; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.local.LocalItemBuilder; - -public class LocalStatisticStreamCardItemHolder extends LocalStatisticStreamItemHolder { - public LocalStatisticStreamCardItemHolder(final LocalItemBuilder infoItemBuilder, - final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_card_item, parent); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java deleted file mode 100644 index 39a43b0344f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamGridItemHolder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.ViewGroup; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.local.LocalItemBuilder; - -public class LocalStatisticStreamGridItemHolder extends LocalStatisticStreamItemHolder { - public LocalStatisticStreamGridItemHolder(final LocalItemBuilder infoItemBuilder, - final ViewGroup parent) { - super(infoItemBuilder, R.layout.list_stream_grid_item, parent); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java deleted file mode 100644 index f26a76ad9f7..00000000000 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.schabi.newpipe.local.holder; - -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.LocalItem; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.ktx.ViewUtils; -import org.schabi.newpipe.local.LocalItemBuilder; -import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.DependentPreferenceHelper; -import org.schabi.newpipe.util.Localization; -import org.schabi.newpipe.util.ServiceHelper; -import org.schabi.newpipe.util.image.CoilHelper; -import org.schabi.newpipe.views.AnimatedProgressBar; - -import java.time.format.DateTimeFormatter; -import java.util.concurrent.TimeUnit; - -/* - * Created by Christian Schabesberger on 01.08.16. - *

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

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

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

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public class LocalStatisticStreamItemHolder extends LocalItemHolder { - public final ImageView itemThumbnailView; - public final TextView itemVideoTitleView; - public final TextView itemUploaderView; - public final TextView itemDurationView; - @Nullable - public final TextView itemAdditionalDetails; - private final AnimatedProgressBar itemProgressView; - - public LocalStatisticStreamItemHolder(final LocalItemBuilder itemBuilder, - final ViewGroup parent) { - this(itemBuilder, R.layout.list_stream_item, parent); - } - - LocalStatisticStreamItemHolder(final LocalItemBuilder infoItemBuilder, final int layoutId, - final ViewGroup parent) { - super(infoItemBuilder, layoutId, parent); - - itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); - itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView); - itemUploaderView = itemView.findViewById(R.id.itemUploaderView); - itemDurationView = itemView.findViewById(R.id.itemDurationView); - itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); - itemProgressView = itemView.findViewById(R.id.itemProgressView); - } - - private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, - final DateTimeFormatter dateTimeFormatter) { - return Localization.concatenateStrings( - // watchCount - Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()), - dateTimeFormatter.format(entry.getLatestAccessDate()), - // serviceName - ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId())); - } - - @Override - public void updateFromItem(final LocalItem localItem, - final HistoryRecordManager historyRecordManager, - final DateTimeFormatter dateTimeFormatter) { - if (!(localItem instanceof StreamStatisticsEntry)) { - return; - } - final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - - itemVideoTitleView.setText(item.getStreamEntity().getTitle()); - itemUploaderView.setText(item.getStreamEntity().getUploader()); - - if (item.getStreamEntity().getDuration() > 0) { - itemDurationView. - setText(Localization.getDurationString(item.getStreamEntity().getDuration())); - itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), - R.color.duration_background_color)); - itemDurationView.setVisibility(View.VISIBLE); - - if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext()) - && item.getProgressMillis() > 0) { - itemProgressView.setVisibility(View.VISIBLE); - itemProgressView.setMax((int) item.getStreamEntity().getDuration()); - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - } else { - itemProgressView.setVisibility(View.GONE); - } - } else { - itemDurationView.setVisibility(View.GONE); - itemProgressView.setVisibility(View.GONE); - } - - if (itemAdditionalDetails != null) { - itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateTimeFormatter)); - } - - // Default thumbnail is shown on error, while loading and if the url is empty - CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, - item.getStreamEntity().getThumbnailUrl()); - - itemView.setOnClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().selected(item); - } - }); - - itemView.setLongClickable(true); - itemView.setOnLongClickListener(view -> { - if (itemBuilder.getOnItemSelectedListener() != null) { - itemBuilder.getOnItemSelectedListener().held(item); - } - return true; - }); - } - - @Override - public void updateState(final LocalItem localItem, - final HistoryRecordManager historyRecordManager) { - if (!(localItem instanceof StreamStatisticsEntry)) { - return; - } - final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - - if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext()) - && item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) { - itemProgressView.setMax((int) item.getStreamEntity().getDuration()); - if (itemProgressView.getVisibility() == View.VISIBLE) { - itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - } else { - itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(item.getProgressMillis())); - ViewUtils.animate(itemProgressView, true, 500); - } - } else if (itemProgressView.getVisibility() == View.VISIBLE) { - ViewUtils.animate(itemProgressView, false, 500); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java deleted file mode 100644 index dc4a403d219..00000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ /dev/null @@ -1,291 +0,0 @@ -package org.schabi.newpipe.settings; - -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; - -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; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -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) { - manager = new ImportExportManager(new BackupFileLocator(requireContext())); - - 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() - ); - - return true; - }); - - final Preference resetSettings = requirePreference(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 - resetSettings.setOnPreferenceClickListener(preference -> { - // 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; - }); - } - - private void requestExportPathResult(final ActivityResult result) { - 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); - - exportDatabase(file, lastExportDataUri); - } - } - - private void requestImportPathResult(final ActivityResult result) { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - // will be saved only on success - final Uri lastImportDataUri = result.getData().getData(); - - final StoredFileHelper file = new StoredFileHelper( - requireContext(), result.getData().getData(), 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(); - } - } - - private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) { - try (ExecutorService executor = Executors.newSingleThreadExecutor()) { - //checkpoint before export - executor.submit(NewPipeDatabase::checkpoint).get(); - - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(requireContext()); - manager.exportDatabase(preferences, file); - - 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"); - } - } - - private void importDatabase(final StoredFileHelper file, final Uri importDataUri) { - // check if file is supported - if (!ZipHelper.isValidZipFile(file)) { - Toast.makeText(requireContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT) - .show(); - return; - } - - try { - manager.ensureDbDirectoryExists(); - - // replace the current database - if (!manager.extractDb(file)) { - Toast.makeText(requireContext(), 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); - 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; - } - cleanImport(context, prefs); - finishImport(importDataUri); - }) - .show(); - } else { - finishImport(importDataUri); - } - } catch (final Exception e) { - showErrorSnackbar(e, "Importing database and settings"); - } - } - - /** - * Remove settings that are not supposed to be imported on different devices - * and reset them to default values. - * @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) { - // 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); - // 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 - // caused by possible later modification of the media tunneling functionality. - // 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); - if (wasMediaTunnelingDisabledAutomatically) { - prefs.edit() - .putInt(automaticTunnelingKey, -1) - .putBoolean(tunnelingKey, false) - .apply(); - NewPipeSettings.setMediaTunneling(context); - } - } - - /** - * Save import path and restart app. - * - * @param importDataUri The import path to save - */ - private void finishImport(final Uri importDataUri) { - // save import path only on success - saveLastImportExportDataUri(importDataUri); - // restart app to properly load db - NavigationHelper.restartApp(requireActivity()); - } - - private Uri getImportExportDataUri() { - final String path = defaultPreferences.getString(importExportDataPathKey, null); - return isBlank(path) ? null : Uri.parse(path); - } - - private void saveLastImportExportDataUri(final Uri importExportDataUri) { - final SharedPreferences.Editor editor = defaultPreferences.edit() - .putString(importExportDataPathKey, importExportDataUri.toString()); - editor.apply(); - } - - private void showErrorSnackbar(final Throwable e, final String request) { - ErrorUtil.showSnackbar(this, new 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) - ); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt new file mode 100644 index 00000000000..9a2299d181b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.kt @@ -0,0 +1,258 @@ +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 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.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() + manager = ImportExportManager(BackupFileLocator(activity)) + + addPreferencesFromResourceRegistry() + + requirePreference(R.string.import_data).setOnPreferenceClickListener { + val picker = StoredFileHelper.getPicker(activity, ZIP_MIME_TYPE, importExportDataUri) + NoFileManagerSafeGuard.launchSafe(requestImportPathLauncher, picker, TAG, activity) + true + } + + 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 + } + + requirePreference(R.string.reset_settings).setOnPreferenceClickListener { + // Show Alert Dialogue + 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 fun requestExportPathResult(result: ActivityResult) { + val context = requireContext() + val lastExportDataUri = result.data?.data + + 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 fun requestImportPathResult(result: ActivityResult) { + val context = requireContext() + val lastImportDataUri = result.data?.data + + if (result.resultCode == Activity.RESULT_OK && lastImportDataUri != null) { + // will be saved only on success + val file = StoredFileHelper(context, lastImportDataUri, ZIP_MIME_TYPE) + + 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 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() + } + + manager.exportDatabase(defaultPreferences, file) + + saveLastImportExportDataUri(exportDataUri) // save export path only on success + Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) + .show() + } + } + + private fun importDatabase(file: StoredFileHelper, importDataUri: Uri) { + val context = requireContext() + + // check if file is supported + if (!ZipHelper.isValidZipFile(file)) { + Toast.makeText(context, R.string.no_valid_zip_file, Toast.LENGTH_SHORT) + .show() + return + } + + try { + manager.ensureDbDirectoryExists() + + // replace the current database + if (!manager.extractDb(file)) { + Toast.makeText(context, R.string.could_not_import_all_files, Toast.LENGTH_LONG) + .show() + } + + // if settings file exist, ask if it should be imported. + val hasJsonPrefs = manager.exportHasJsonPrefs(file) + if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) { + 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) + } + } 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) + } + } catch (e: Exception) { + showErrorSnackbar(e, "Importing database and settings") + } + } + + /** + * Remove settings that are not supposed to be imported on different devices + * and reset them to default values. + * @param context the context used for the import + * @param prefs the preferences used while running the import + */ + 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. + 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 + // caused by possible later modification of the media tunneling functionality. + // 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 + val wasMediaTunnelingDisabledAutomatically = + prefs.getInt(automaticTunnelingKey, -1) == 1 && + prefs.getBoolean(tunnelingKey, false) + if (wasMediaTunnelingDisabledAutomatically) { + prefs.edit { + putInt(automaticTunnelingKey, -1) + putBoolean(tunnelingKey, false) + } + NewPipeSettings.setMediaTunneling(context) + } + } + + /** + * Save import path and restart app. + * + * @param importDataUri The import path to save + */ + private fun finishImport(importDataUri: Uri) { + // save import path only on success + saveLastImportExportDataUri(importDataUri) + // restart app to properly load db + NavigationHelper.restartApp(requireActivity()) + } + + 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 fun showErrorSnackbar(e: Throwable, request: String) { + showSnackbar(this, ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)) + } + + private fun createErrorNotification(e: Throwable, request: String) { + createNotification( + requireContext(), + ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request) + ) + } + + companion object { + private const val ZIP_MIME_TYPE = "application/zip" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java index 9bc9058c803..84a140247d9 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/HistorySettingsFragment.java @@ -20,6 +20,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; public class HistorySettingsFragment extends BasePreferenceFragment { private String cacheWipeKey; @@ -79,8 +80,8 @@ private static Disposable getDeletePlaybackStatesDisposable( return recordManager.deleteCompleteStreamStateHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> Toast.makeText(context, - R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show(), + () -> Toast.makeText(context, R.string.watch_history_states_deleted, + Toast.LENGTH_SHORT).show(), throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Delete playback states"))); @@ -91,7 +92,7 @@ private static Disposable getWholeStreamHistoryDisposable( return recordManager.deleteWholeStreamHistory() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> Toast.makeText(context, + () -> Toast.makeText(context, R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(), throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, @@ -103,7 +104,7 @@ private static Disposable getRemoveOrphanedRecordsDisposable( return recordManager.removeOrphanedRecords() .observeOn(AndroidSchedulers.mainThread()) .subscribe( - howManyDeleted -> { }, + Functions.EMPTY_ACTION, throwable -> ErrorUtil.openActivity(context, new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, "Clear orphaned records"))); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java index 4c1f65df27c..d11a9cdfbf8 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; @@ -305,8 +305,8 @@ public int getTabIconRes(final Context context) { } @Override - public StatisticsPlaylistFragment getFragment(final Context context) { - return new StatisticsPlaylistFragment(); + public Fragment getFragment(final Context context) { + return new HistoryFragment(); } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt b/app/src/main/java/org/schabi/newpipe/ui/components/about/LicenseDialog.kt index bc736e5cd9a..284aa38ede3 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 diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt new file mode 100644 index 00000000000..0388c4b1852 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/DropdownTextMenuItem.kt @@ -0,0 +1,20 @@ +package org.schabi.newpipe.ui.components.common + +import androidx.annotation.StringRes +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.res.stringResource + +@Composable +@NonRestartableComposable +fun DropdownTextMenuItem( + @StringRes text: Int, + onClick: () -> Unit +) { + DropdownMenuItem( + text = { Text(text = stringResource(text)) }, + onClick = onClick + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt new file mode 100644 index 00000000000..f3a23f62f2b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/IconButtonWithLabel.kt @@ -0,0 +1,52 @@ +package org.schabi.newpipe.ui.components.common + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun IconButtonWithLabel( + icon: ImageVector, + @StringRes label: Int, + onClick: () -> Unit, +) { + FilledTonalButton( + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp), + onClick = onClick + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = icon, contentDescription = null) + Text(text = stringResource(label)) + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun IconButtonWithLabelPreview() { + AppTheme { + Surface { + IconButtonWithLabel(Icons.Default.Info, R.string.name) {} + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt b/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt new file mode 100644 index 00000000000..ee8f47d482b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/common/PlaybackControlButtons.kt @@ -0,0 +1,43 @@ +package org.schabi.newpipe.ui.components.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.PlaylistPlay +import androidx.compose.material.icons.filled.Headphones +import androidx.compose.material.icons.filled.PictureInPicture +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.R +import org.schabi.newpipe.ktx.findFragmentActivity +import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.util.NavigationHelper + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PlaybackControlButtons(queue: PlayQueue) { + val context = LocalContext.current + + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally)) { + IconButtonWithLabel( + icon = Icons.Default.Headphones, + label = R.string.controls_background_title, + onClick = { NavigationHelper.playOnBackgroundPlayer(context, queue, false) }, + ) + + IconButtonWithLabel( + icon = Icons.AutoMirrored.Filled.PlaylistPlay, + label = R.string.play_all, + onClick = { NavigationHelper.playOnMainPlayer(context.findFragmentActivity(), queue) }, + ) + + IconButtonWithLabel( + icon = Icons.Default.PictureInPicture, + label = R.string.controls_popup_title, + onClick = { NavigationHelper.playOnPopupPlayer(context, queue, false) }, + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt new file mode 100644 index 00000000000..bbbff457da4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/Info.kt @@ -0,0 +1,22 @@ +package org.schabi.newpipe.ui.components.items + +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.util.NO_SERVICE_ID + +sealed class Info + +class Playlist( + val serviceId: Int = NO_SERVICE_ID, + val url: String = "", + val name: String = "", + val thumbnails: List = emptyList(), + val uploaderName: String = "", + val streamCount: Long = 10, +) : Info() { + + constructor(item: PlaylistInfoItem) : this( + item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), + item.streamCount + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/ItemList.kt index ba45c503dd3..5487c53af5b 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,44 +1,59 @@ package org.schabi.newpipe.ui.components.items +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems import androidx.preference.PreferenceManager import androidx.window.core.layout.WindowWidthSizeClass +import my.nanihadesuka.compose.LazyVerticalGridScrollbar import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.InfoItem -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem -import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.ui.components.common.LazyColumnThemedScrollbar +import org.schabi.newpipe.ui.components.common.LoadingIndicator +import org.schabi.newpipe.ui.components.common.defaultThemedScrollbarSettings import org.schabi.newpipe.ui.components.items.playlist.PlaylistListItem +import org.schabi.newpipe.ui.components.items.stream.StreamCardItem +import org.schabi.newpipe.ui.components.items.stream.StreamGridItem import org.schabi.newpipe.ui.components.items.stream.StreamListItem +import org.schabi.newpipe.ui.emptystate.EmptyStateComposable +import org.schabi.newpipe.ui.emptystate.EmptyStateSpec import org.schabi.newpipe.util.DependentPreferenceHelper import org.schabi.newpipe.util.NavigationHelper @Composable fun ItemList( - items: List, + items: LazyPagingItems, mode: ItemViewMode = determineItemViewMode(), - listHeader: LazyListScope.() -> Unit = {} + header: @Composable () -> Unit = {}, ) { val context = LocalContext.current val onClick = remember { - { item: InfoItem -> + { item: Info -> val fragmentManager = context.findFragmentActivity().supportFragmentManager - if (item is StreamInfoItem) { + if (item is Stream) { NavigationHelper.openVideoDetailFragment( context, fragmentManager, @@ -48,7 +63,7 @@ fun ItemList( null, false ) - } else if (item is PlaylistInfoItem) { + } else if (item is Playlist) { NavigationHelper.openPlaylistFragment( fragmentManager, item.serviceId, @@ -61,9 +76,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 } } @@ -76,30 +91,75 @@ fun ItemList( val showProgress = DependentPreferenceHelper.getPositionsInListsEnabled(context) val nestedScrollModifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()) - if (mode == ItemViewMode.GRID) { - // TODO: Implement grid layout using LazyVerticalGrid and LazyVerticalGridScrollbar. + if (items.loadState.refresh is LoadState.NotLoading && items.itemCount == 0) { + EmptyStateComposable( + spec = EmptyStateSpec.NoVideos, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 128.dp) + ) + } else if (mode == ItemViewMode.GRID) { + val state = rememberLazyGridState() + + LazyVerticalGridScrollbar(state = state, settings = defaultThemedScrollbarSettings()) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + val isCompact = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT + val minWidth = if (isCompact) 150.dp else 250.dp + + LazyVerticalGrid( + modifier = nestedScrollModifier, + state = state, + columns = GridCells.Adaptive(minWidth) + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + header() + } + + items(items.itemCount) { + val item = items[it] + val isSelected = selectedStream == item + + // TODO: Implement playlist and channel grid items. + if (item is Stream) { + StreamGridItem( + item, showProgress, isSelected, isCompact, onClick, onLongClick, + onDismissPopup + ) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.size(minWidth, if (isCompact) 150.dp else 200.dp)) + } + } + } + } } else { val state = rememberLazyListState() LazyColumnThemedScrollbar(state = state) { LazyColumn(modifier = nestedScrollModifier, state = state) { - listHeader() + item { + header() + } - items(items.size) { + items(items.itemCount) { val item = items[it] - if (item is StreamInfoItem) { + // TODO: Implement playlist and channel items. + if (item is Stream) { val isSelected = selectedStream == item - StreamListItem( - item, - showProgress, - isSelected, - onClick, - onLongClick, - onDismissPopup - ) - } else if (item is PlaylistInfoItem) { + + if (mode == ItemViewMode.CARD) { + StreamCardItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup + ) + } else { + StreamListItem( + item, showProgress, isSelected, onClick, onLongClick, onDismissPopup + ) + } + } else if (item is Playlist) { PlaylistListItem(item, onClick) + } else if (item == null) { // Placeholder + LoadingIndicator(Modifier.height(80.dp)) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt new file mode 100644 index 00000000000..dda3359fb3d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/Stream.kt @@ -0,0 +1,83 @@ +package org.schabi.newpipe.ui.components.items + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.schabi.newpipe.App +import org.schabi.newpipe.database.stream.StreamStatisticsEntry +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.image.ImageStrategy +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.concurrent.TimeUnit + +@Parcelize +class Stream( + val serviceId: Int = NO_SERVICE_ID, + val url: String = "", + val name: String = "", + val thumbnails: List = emptyList(), + val uploaderName: String = "", + val type: StreamType, + val uploaderUrl: String? = null, + val duration: Long = TimeUnit.HOURS.toSeconds(1), + val detailText: String = "", + val streamId: Long = -1, +) : Info(), Parcelable { + + constructor(item: StreamInfoItem) : this( + item.serviceId, item.url, item.name, item.thumbnails, item.uploaderName.orEmpty(), + item.streamType, item.uploaderUrl, item.duration, item.detailText + ) + + constructor(entry: StreamStatisticsEntry) : this( + entry.streamEntity.serviceId, entry.streamEntity.url, entry.streamEntity.title, + ImageStrategy.dbUrlToImageList(entry.streamEntity.thumbnailUrl), entry.streamEntity.uploader, + entry.streamEntity.streamType, entry.streamEntity.uploaderUrl, entry.streamEntity.duration, + entry.detailText, entry.streamId + ) + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(serviceId, url, name, type) + item.duration = duration + item.uploaderName = uploaderName + item.uploaderUrl = uploaderUrl + item.thumbnails = thumbnails + return item + } +} + +private val StreamInfoItem.detailText: String + get() { + val context = App.instance + val views = if (viewCount >= 0) { + when (streamType) { + StreamType.AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount) + StreamType.LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount) + else -> Localization.shortViewCount(context, viewCount) + } + } else { + "" + } + val date = Localization.relativeTimeOrTextual(context, uploadDate, textualUploadDate) + + return if (views.isEmpty()) { + date.orEmpty() + } else if (date.isNullOrEmpty()) { + views + } else { + "$views • $date" + } + } + +private val StreamStatisticsEntry.detailText: String + get() = + Localization.concatenateStrings( + Localization.shortViewCount(App.instance, watchCount), + DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(latestAccessDate), + ServiceHelper.getNameOfServiceById(streamEntity.serviceId), + ) diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt new file mode 100644 index 00000000000..27c8915f294 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/common/Thumbnail.kt @@ -0,0 +1,70 @@ +package org.schabi.newpipe.ui.components.items.common + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.util.image.ImageStrategy + +@Composable +fun Thumbnail( + images: List, + imageDescription: String, + @DrawableRes imagePlaceholder: Int, + cornerBackgroundColor: Color, + cornerIcon: ImageVector?, + cornerText: String, + contentScale: ContentScale, + modifier: Modifier = Modifier +) { + Box(contentAlignment = Alignment.BottomEnd) { + AsyncImage( + model = ImageStrategy.choosePreferredImage(images), + contentDescription = imageDescription, + placeholder = painterResource(imagePlaceholder), + error = painterResource(imagePlaceholder), + contentScale = contentScale, + modifier = modifier + ) + + Row( + modifier = Modifier + .padding(2.dp) + .background(cornerBackgroundColor) + .padding(2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (cornerIcon != null) { + Icon( + imageVector = cornerIcon, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + } + + Text( + text = cornerText, + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt index c8a9b2d0e1b..73ac89f50af 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistListItem.kt @@ -17,15 +17,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.schabi.newpipe.extractor.InfoItem -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.ui.components.items.Playlist import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID @Composable fun PlaylistListItem( - playlist: PlaylistInfoItem, - onClick: (InfoItem) -> Unit = {} + playlist: Playlist, + onClick: (Playlist) -> Unit = {}, ) { Row( modifier = Modifier @@ -49,7 +48,7 @@ fun PlaylistListItem( ) Text( - text = playlist.uploaderName.orEmpty(), + text = playlist.uploaderName, style = MaterialTheme.typography.bodySmall ) } @@ -60,8 +59,7 @@ fun PlaylistListItem( @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun PlaylistListItemPreview() { - val playlist = PlaylistInfoItem(NO_SERVICE_ID, "", "Playlist") - playlist.uploaderName = "Uploader" + val playlist = Playlist(NO_SERVICE_ID, "", "Playlist", uploaderName = "Uploader") AppTheme { Surface { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt index 36711105b27..e11c089aa40 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/playlist/PlaylistThumbnail.kt @@ -1,66 +1,32 @@ package org.schabi.newpipe.ui.components.items.playlist -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage +import androidx.compose.ui.res.stringResource import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.ui.components.items.Playlist +import org.schabi.newpipe.ui.components.items.common.Thumbnail import org.schabi.newpipe.util.Localization -import org.schabi.newpipe.util.image.ImageStrategy @Composable fun PlaylistThumbnail( - playlist: PlaylistInfoItem, + playlist: Playlist, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit ) { - Box(contentAlignment = Alignment.BottomEnd) { - AsyncImage( - model = ImageStrategy.choosePreferredImage(playlist.thumbnails), - contentDescription = null, - placeholder = painterResource(R.drawable.placeholder_thumbnail_playlist), - error = painterResource(R.drawable.placeholder_thumbnail_playlist), - contentScale = contentScale, - modifier = modifier - ) - - Row( - modifier = Modifier - .padding(2.dp) - .background(Color.Black.copy(alpha = 0.5f)) - .padding(2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.AutoMirrored.Default.PlaylistPlay, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(18.dp) - ) - - val context = LocalContext.current - Text( - text = Localization.localizeStreamCountMini(context, playlist.streamCount), - color = Color.White, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 4.dp) - ) - } - } + Thumbnail( + images = playlist.thumbnails, + imageDescription = stringResource(R.string.playlist_content_description, playlist.name), + imagePlaceholder = R.drawable.placeholder_thumbnail_playlist, + cornerBackgroundColor = Color.Black.copy(alpha = 0.5f), + cornerIcon = Icons.AutoMirrored.Default.PlaylistPlay, + cornerText = Localization.localizeStreamCountMini(LocalContext.current, playlist.streamCount), + contentScale = contentScale, + modifier = modifier + ) } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt new file mode 100644 index 00000000000..67f2f9442fb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamCardItem.kt @@ -0,0 +1,91 @@ +package org.schabi.newpipe.ui.components.items.stream + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.theme.AppTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StreamCardItem( + stream: Stream, + showProgress: Boolean, + isSelected: Boolean, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } + ) + .padding(top = 12.dp, start = 2.dp, end = 2.dp) + ) { + StreamThumbnail( + stream = stream, + showProgress = showProgress, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + + Column(modifier = Modifier.padding(10.dp)) { + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stream.uploaderName, + style = MaterialTheme.typography.bodySmall + ) + + Text( + text = stream.detailText, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StreamCardItemPreview( + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream +) { + AppTheme { + Surface { + StreamCardItem(stream, showProgress = false, isSelected = false) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt new file mode 100644 index 00000000000..3ae3b137989 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamGridItem.kt @@ -0,0 +1,81 @@ +package org.schabi.newpipe.ui.components.items.stream + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.theme.AppTheme + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StreamGridItem( + stream: Stream, + showProgress: Boolean, + isSelected: Boolean = false, + isMini: Boolean = false, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, + onDismissPopup: () -> Unit = {} +) { + Box { + Column( + modifier = Modifier + .combinedClickable( + onLongClick = { onLongClick(stream) }, + onClick = { onClick(stream) } + ) + .padding(12.dp) + ) { + val size = if (isMini) DpSize(150.dp, 85.dp) else DpSize(246.dp, 138.dp) + + StreamThumbnail( + stream = stream, + showProgress = showProgress, + modifier = Modifier.size(size) + ) + + Text( + text = stream.name, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + maxLines = 2 + ) + + Text(text = stream.uploaderName, style = MaterialTheme.typography.bodySmall) + + Text( + text = stream.detailText, + style = MaterialTheme.typography.bodySmall + ) + } + + StreamMenu(stream, isSelected, onDismissPopup) + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StreamGridItemPreview( + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream +) { + AppTheme { + Surface { + StreamGridItem(stream, showProgress = false, isSelected = false) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt new file mode 100644 index 00000000000..e2f698ade77 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamItemPreviewProvider.kt @@ -0,0 +1,13 @@ +package org.schabi.newpipe.ui.components.items.stream + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import org.schabi.newpipe.extractor.stream.StreamType +import org.schabi.newpipe.ui.components.items.Stream + +internal class StreamItemPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + Stream(type = StreamType.NONE, name = "Stream", uploaderName = "Uploader"), + Stream(type = StreamType.LIVE_STREAM, name = "Stream", uploaderName = "Uploader"), + Stream(type = StreamType.AUDIO_LIVE_STREAM, name = "Stream", uploaderName = "Uploader"), + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt index 84fff3e74cf..b15fef2ee0a 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamListItem.kt @@ -20,17 +20,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.theme.AppTheme @OptIn(ExperimentalFoundationApi::class) @Composable fun StreamListItem( - stream: StreamInfoItem, + stream: Stream, showProgress: Boolean, isSelected: Boolean, - onClick: (StreamInfoItem) -> Unit = {}, - onLongClick: (StreamInfoItem) -> Unit = {}, + onClick: (Stream) -> Unit = {}, + onLongClick: (Stream) -> Unit = {}, onDismissPopup: () -> Unit = {} ) { // Box serves as an anchor for the dropdown menu @@ -58,10 +58,10 @@ fun StreamListItem( maxLines = 2 ) - Text(text = stream.uploaderName.orEmpty(), style = MaterialTheme.typography.bodySmall) + Text(text = stream.uploaderName, style = MaterialTheme.typography.bodySmall) Text( - text = getStreamInfoDetail(stream), + text = stream.detailText, style = MaterialTheme.typography.bodySmall ) } @@ -75,7 +75,7 @@ fun StreamListItem( @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun StreamListItemPreview( - @PreviewParameter(StreamItemPreviewProvider::class) stream: StreamInfoItem + @PreviewParameter(StreamItemPreviewProvider::class) stream: Stream ) { AppTheme { Surface { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt index 099a9300508..277fc35c5bf 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt @@ -1,20 +1,18 @@ package org.schabi.newpipe.ui.components.items.stream import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel import org.schabi.newpipe.R import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.download.DownloadDialog -import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.ktx.findFragmentActivity import org.schabi.newpipe.local.dialog.PlaylistAppendDialog import org.schabi.newpipe.local.dialog.PlaylistDialog import org.schabi.newpipe.player.helper.PlayerHolder +import org.schabi.newpipe.ui.components.common.DropdownTextMenuItem +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.SparseItemUtil import org.schabi.newpipe.util.external_communication.ShareUtils @@ -22,58 +20,70 @@ 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() DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { if (PlayerHolder.isPlayQueueReady) { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.enqueue_stream)) }, + DropdownTextMenuItem( + text = R.string.enqueue_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.enqueueOnPlayer(context, it) } - } + }, ) if (PlayerHolder.queuePosition < PlayerHolder.queueSize - 1) { - DropdownMenuItem( - text = { Text(text = stringResource(R.string.enqueue_next_stream)) }, + DropdownTextMenuItem( + text = R.string.enqueue_stream, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { - NavigationHelper.enqueueNextOnPlayer(context, it) + SparseItemUtil.fetchItemInfoIfSparse(context, info) { + NavigationHelper.enqueueOnPlayer(context, it) } - } + }, ) } } - DropdownMenuItem( - text = { Text(text = stringResource(R.string.start_here_on_background)) }, + DropdownTextMenuItem( + text = R.string.start_here_on_background, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnBackgroundPlayer(context, it, true) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.start_here_on_popup)) }, + DropdownTextMenuItem( + text = R.string.start_here_on_popup, onClick = { onDismissRequest() - SparseItemUtil.fetchItemInfoIfSparse(context, stream) { + SparseItemUtil.fetchItemInfoIfSparse(context, info) { NavigationHelper.playOnPopupPlayer(context, it, true) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.download)) }, + + if (stream.streamId != -1L) { + DropdownTextMenuItem( + text = R.string.delete, + onClick = { + onDismissRequest() + streamViewModel.deleteStreamHistory(stream.streamId) + }, + ) + } + + DropdownTextMenuItem( + text = R.string.download, onClick = { onDismissRequest() SparseItemUtil.fetchStreamInfoAndSaveToDatabase( @@ -86,45 +96,45 @@ fun StreamMenu( val fragmentManager = context.findFragmentActivity().supportFragmentManager downloadDialog.show(fragmentManager, "downloadDialog") } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.add_to_playlist)) }, + DropdownTextMenuItem( + text = R.string.add_to_playlist, onClick = { onDismissRequest() - val list = listOf(StreamEntity(stream)) + val list = listOf(StreamEntity(info)) PlaylistDialog.createCorrespondingDialog(context, list) { dialog -> val tag = if (dialog is PlaylistAppendDialog) "append" else "create" dialog.show( context.findFragmentActivity().supportFragmentManager, - "StreamDialogEntry@${tag}_playlist" + "StreamDialogEntry@${tag}_playlist", ) } - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.share)) }, + DropdownTextMenuItem( + text = R.string.share, onClick = { onDismissRequest() ShareUtils.shareText(context, stream.name, stream.url, stream.thumbnails) - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.open_in_browser)) }, + DropdownTextMenuItem( + text = R.string.open_in_browser, onClick = { onDismissRequest() ShareUtils.openUrlInBrowser(context, stream.url) - } + }, ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.mark_as_watched)) }, + DropdownTextMenuItem( + text = R.string.mark_as_watched, onClick = { onDismissRequest() - streamViewModel.markAsWatched(stream) + streamViewModel.markAsWatched(info) } ) - DropdownMenuItem( - text = { Text(text = stringResource(R.string.show_channel_details)) }, + DropdownTextMenuItem( + text = R.string.show_channel_details, onClick = { onDismissRequest() SparseItemUtil.fetchUploaderUrlIfSparse( @@ -134,7 +144,7 @@ fun StreamMenu( 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 9ee5e0d3ff7..cb70e86c8d8 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,24 +1,17 @@ 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 @@ -26,52 +19,42 @@ import coil3.compose.AsyncImage import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.ui.components.items.Stream +import org.schabi.newpipe.ui.components.items.common.Thumbnail import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.StreamTypeUtil -import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.viewmodels.StreamViewModel @Composable fun StreamThumbnail( - stream: StreamInfoItem, + stream: Stream, showProgress: Boolean, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit ) { Column(modifier = modifier) { - Box(contentAlignment = Alignment.BottomEnd) { - AsyncImage( - model = ImageStrategy.choosePreferredImage(stream.thumbnails), - contentDescription = null, - placeholder = painterResource(R.drawable.placeholder_thumbnail_video), - error = painterResource(R.drawable.placeholder_thumbnail_video), - contentScale = contentScale, - modifier = modifier - ) - - val isLive = StreamTypeUtil.isLiveStream(stream.streamType) - Text( - modifier = Modifier - .padding(2.dp) - .background(if (isLive) Color.Red else Color.Black.copy(alpha = 0.5f)) - .padding(2.dp), - text = if (isLive) { - stringResource(R.string.duration_live) - } else { - Localization.getDurationString(stream.duration) - }, - color = Color.White, - style = MaterialTheme.typography.bodySmall - ) - } + val isLive = StreamTypeUtil.isLiveStream(stream.type) + Thumbnail( + images = stream.thumbnails, + imageDescription = stringResource(R.string.stream_content_description, stream.name), + imagePlaceholder = R.drawable.placeholder_thumbnail_video, + cornerBackgroundColor = if (isLive) Color.Red else Color.Black.copy(alpha = 0.5f), + cornerIcon = null, + cornerText = if (isLive) { + stringResource(R.string.duration_live) + } else { + Localization.getDurationString(stream.duration) + }, + contentScale = contentScale, + modifier = modifier + ) if (showProgress) { val streamViewModel = viewModel() var progress by rememberSaveable { mutableLongStateOf(0L) } LaunchedEffect(stream) { - progress = streamViewModel.getStreamState(stream)?.progressMillis ?: 0L + progress = streamViewModel.getStreamState(stream.toStreamInfoItem())?.progressMillis ?: 0L } if (progress != 0L) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt index 3f444a9d93b..578ea3170a6 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/video/RelatedItems.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -21,29 +20,43 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.edit +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.flowOf import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.ui.components.items.ItemList -import org.schabi.newpipe.ui.components.items.stream.StreamInfoItem -import org.schabi.newpipe.ui.emptystate.EmptyStateComposable -import org.schabi.newpipe.ui.emptystate.EmptyStateSpec +import org.schabi.newpipe.ui.components.items.Playlist +import org.schabi.newpipe.ui.components.items.Stream import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.NO_SERVICE_ID +import java.util.concurrent.TimeUnit @Composable fun RelatedItems(info: StreamInfo) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(LocalContext.current) + val context = LocalContext.current + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val key = stringResource(R.string.auto_queue_key) // TODO: AndroidX DataStore might be a better option. var isAutoQueueEnabled by rememberSaveable { mutableStateOf(sharedPreferences.getBoolean(key, false)) } + val displayItems = info.relatedItems.mapNotNull { + when (it) { + is StreamInfoItem -> Stream(it) + is PlaylistInfoItem -> Playlist(it) + else -> null + } + } ItemList( - items = info.relatedItems, + items = flowOf(PagingData.from(displayItems)).collectAsLazyPagingItems(), mode = ItemViewMode.LIST, listHeader = { item { @@ -56,30 +69,19 @@ fun RelatedItems(info: StreamInfo) { ) { 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) - } + 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) } - ) - } - } - } - if (info.relatedItems.isEmpty()) { - item { - EmptyStateComposable( - spec = EmptyStateSpec.NoVideos, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 128.dp) + } ) } } @@ -87,6 +89,26 @@ fun RelatedItems(info: StreamInfo) { ) } +private fun StreamInfoItem( + serviceId: Int = NO_SERVICE_ID, + url: String = "", + name: String = "Stream", + streamType: StreamType, + uploaderName: String? = "Uploader", + uploaderUrl: String? = null, + uploaderAvatars: List = emptyList(), + duration: Long = TimeUnit.HOURS.toSeconds(1), + viewCount: Long = 10, + textualUploadDate: String = "1 month ago" +) = StreamInfoItem(serviceId, url, name, streamType).apply { + this.uploaderName = uploaderName + this.uploaderUrl = uploaderUrl + this.uploaderAvatars = uploaderAvatars + this.duration = duration + this.viewCount = viewCount + this.textualUploadDate = textualUploadDate +} + @Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt index 75e013ad1ea..8916c3ac37c 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/emptystate/EmptyStateUtil.kt @@ -4,10 +4,6 @@ package org.schabi.newpipe.ui.emptystate import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy @@ -22,17 +18,12 @@ fun ComposeView.setEmptyStateComposable( setViewCompositionStrategy(strategy) setContent { AppTheme { - CompositionLocalProvider( - LocalContentColor provides contentColorFor(MaterialTheme.colorScheme.background) - ) { - EmptyStateComposable( - spec = spec, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 128.dp) - - ) - } + EmptyStateComposable( + spec = spec, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 128.dp) + ) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt index 673a228928a..a2580a1c5c3 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/AboutScreen.kt @@ -77,7 +77,7 @@ fun AboutScreen(padding: PaddingValues) { @Composable private fun AboutScreenPreview() { AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { + Surface { AboutScreen(PaddingValues(8.dp)) } } diff --git a/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt new file mode 100644 index 00000000000..55f44293811 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/screens/HistoryScreen.kt @@ -0,0 +1,105 @@ +package org.schabi.newpipe.ui.screens + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.collectAsLazyPagingItems +import org.schabi.newpipe.R +import org.schabi.newpipe.local.history.HistoryViewModel +import org.schabi.newpipe.local.history.SortKey +import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.player.playqueue.SinglePlayQueue +import org.schabi.newpipe.ui.components.common.PlaybackControlButtons +import org.schabi.newpipe.ui.components.items.ItemList +import org.schabi.newpipe.ui.theme.AppTheme + +@Composable +fun HistoryScreen(viewModel: HistoryViewModel = viewModel()) { + val sortKey by viewModel.sortKey.collectAsStateWithLifecycle() + val historyItems = viewModel.historyItems.collectAsLazyPagingItems() + val streams = historyItems.itemSnapshotList.mapNotNull { it?.toStreamInfoItem() } + val queue = SinglePlayQueue(streams, 0) + + ItemList(historyItems, header = { + HistoryHeader( + sortKey = sortKey, + onSelectSortKey = viewModel::updateOrder, + queue = queue + ) + }) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun HistoryHeader( + sortKey: SortKey, + queue: PlayQueue, + onSelectSortKey: (SortKey) -> Unit, +) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + ) { + HistorySortRow(sortKey, onSelectSortKey) + + PlaybackControlButtons(queue) + } +} + +@Composable +private fun HistorySortRow( + sortKey: SortKey, + onSelectSortKey: (SortKey) -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(R.string.history_sort_label)) + + SingleChoiceSegmentedButtonRow { + SortKey.entries.forEachIndexed { index, key -> + SegmentedButton( + selected = key == sortKey, + onClick = { onSelectSortKey(key) }, + shape = SegmentedButtonDefaults + .itemShape(index = index, count = SortKey.entries.size) + ) { + Text(text = stringResource(key.title)) + } + } + } + } +} + +@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun HistoryHeaderPreview() { + AppTheme { + Surface { + HistoryHeader(SortKey.MOST_PLAYED, SinglePlayQueue(listOf(), 0)) {} + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt index 208dbc89506..acad06c3621 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/theme/Theme.kt @@ -85,7 +85,10 @@ private val darkScheme = darkColorScheme( surfaceContainerHighest = surfaceContainerHighestDark ) -private val blackScheme = darkScheme.copy(surface = Color.Black) +private val blackScheme = darkScheme.copy( + background = Color.Black, + surface = Color.Black +) @Composable fun AppTheme(useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 0a7906b8d75..0c70a2e658b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -15,7 +15,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -52,7 +51,7 @@ import org.schabi.newpipe.ktx.ContextKt; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; -import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.history.HistoryFragment; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; @@ -119,7 +118,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) { @@ -555,7 +554,7 @@ public static void openLocalPlaylistFragment(final FragmentManager fragmentManag public static void openStatisticFragment(final FragmentManager fragmentManager) { defaultTransaction(fragmentManager) - .replace(R.id.fragment_holder, new StatisticsPlaylistFragment()) + .replace(R.id.fragment_holder, HistoryFragment.class, null, null) .addToBackStack(null) .commit(); } diff --git a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt index fff8d6b71fa..c601450a0c3 100644 --- a/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/viewmodels/StreamViewModel.kt @@ -3,6 +3,7 @@ package org.schabi.newpipe.viewmodels import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import kotlinx.coroutines.rx3.awaitSingleOrNull @@ -23,4 +24,10 @@ class StreamViewModel(application: Application) : AndroidViewModel(application) historyRecordManager.markAsWatched(stream).await() } } + + fun deleteStreamHistory(streamId: Long) { + viewModelScope.launch(Dispatchers.IO) { + historyRecordManager.deleteStreamHistoryAndState(streamId).await() + } + } } diff --git a/app/src/main/res/layout/statistic_playlist_control.xml b/app/src/main/res/layout/statistic_playlist_control.xml index 36540d32e91..f3491078e23 100644 --- a/app/src/main/res/layout/statistic_playlist_control.xml +++ b/app/src/main/res/layout/statistic_playlist_control.xml @@ -30,7 +30,7 @@ android:layout_height="50dp" android:layout_toRightOf="@id/sortButtonIcon" android:gravity="left|center" - android:text="@string/title_most_played" + android:text="@string/history_sort_views" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="15sp" android:textStyle="bold" diff --git a/app/src/main/res/values-ar-rLY/strings.xml b/app/src/main/res/values-ar-rLY/strings.xml index 2c2e0e9084c..177e3bc7cf8 100644 --- a/app/src/main/res/values-ar-rLY/strings.xml +++ b/app/src/main/res/values-ar-rLY/strings.xml @@ -403,7 +403,6 @@ اجراء الإيماءة اليمنى الرموز المسموح بها في أسماء الملفات %1$s %2$s - آخر ما تم تشغيله استخدم دائمًا الحل البديل لإعداد سطح إخراج فيديو ExoPlayer البث التالي تم تعطيل نفق وسائل الإعلام عن طريق التقصير على جهازك لأن نموذج جهازك معروف بأنه لا يدعمه. @@ -493,7 +492,6 @@ عرض نتائج ل: %s افتح باستخدام هل تريد حذف هذا العنصر من سجل البحث؟ - الأكثر تشغيلا عرض الوقت الأصلي على العناصر استعادة مِن الدقة الافتراضية diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 302c4f414a4..a15c82462aa 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -283,8 +283,6 @@ لا يوجد بث متاح للتنزيل تم حذف عنصر واحد. NewPipe هو برنامج مفتوح المصدر وبحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. وعلى وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار أحدث. - آخر ما تم تشغيله - الأكثر تشغيلا هذا سوف يُزيل إعداداتك الحالية. طريقة \'التشغيل\' المفضلة الإجراء الافتراضي عند فتح المحتوى — %s @@ -568,7 +566,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 37768e16fb9..28cfeb5cc12 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -332,8 +332,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 81b212f800e..7a3f76639ae 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -408,8 +408,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 2be37ea7c93..d525d7e86c8 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 ef7ef98c3d7..cfccf299d0d 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -229,9 +229,7 @@ Гісторыя Гісторыя Выдаліць гэты элемент з гісторыі пошуку? - Прайгравалася нядаўна - Прайгравалася найбольш - Змесціва галоўнай старонкі + Кантэнт галоўнай старонкі Пустая старонка Старонка кіёска Старонка канала diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ac34b120bb3..f69cbe5eeba 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -221,8 +221,6 @@ История История Искате ли да изтриете този елемент от историята на търсенията? - Последно възпроизвеждани - Най-възпроизвеждани Съдържание на главната страница Празна страница Страница-павилион diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 2b0ffe91874..b68fef77c2e 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -192,8 +192,6 @@ একটি চ্যানেল পছন্দ করুন চ্যানেল এর পাতা খালি পাতা - সবথেকে বেশি চালানো - শেষ চালানো লাইসেন্স পড়ুন নিউপাইপ এর লাইসেন্স প্রাইভেসি পলিসি পড়ুন diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index a79319ee35f..6268487d199 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 68ab8a709ff..c7b5c2f6f27 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 f67bfb3cb7c..ec8cf9468d9 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -197,8 +197,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 1e8fee69494..eeee36594d7 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -99,7 +99,6 @@ مۆڵەتنامەی نیوپایپ پیشاندانی ڕێنمایی ”داگرتن تا پاشکۆ” دابەشکراوەکان - زۆرترین لێدراو لادانی نیشانه‌كراو مۆڵەتەکان ناتوانرێت به‌ژداریكردنه‌كه‌ نوێبكرێته‌وه‌ @@ -309,7 +308,6 @@ مێژوو دەسڕێتەوە لەگەڵ په‌خشه‌ لێدراوه‌كان و شوێنی کارپێکەر دانان-خۆکار ١ بابەت سڕایەوە. - کرداری بنەڕەتی لەکاتی کردنەوەی بابەتدا — %s هکیۆسکێک دیار بکە کۆنفرانسەکان كردنه‌وه‌ له‌ دۆخی په‌نجه‌ره‌ @@ -503,7 +501,6 @@ ڕێکخستنەکانی دەنگ پرست پێ دەکرێت بۆ شوێنی دابەزاندنی هەر بابەتێک. \nهەڵبژێرەری فۆڵدەری سیستەم کارابکە (SAF) گەر دەتەوێت بابەتەکانت لە بیرگەی دەرەکیدا داببەزێنرێن - دواین لێدراو ناتوانرێ لیستی دابه‌زاندن دابنرێت وەشانی نوێی نیوپایپ بەردەستە! وێنۆچکەی خشتەلێدان گۆڕدرا. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1c9be53f863..2e07d065438 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -222,8 +222,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 e4875e7d24d..97727a589b9 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 81ad25358be..8ab5906b489 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -217,8 +217,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 0ec6d62aa89..e31ba960170 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -225,8 +225,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 3dde6961882..7be1ef74c10 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -315,8 +315,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 5c80381bbca..60f0c6cd4a9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -222,8 +222,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 05c1bb82d33..5e395452775 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -210,8 +210,6 @@ Ajalugu Ajalugu Kas kustutame selle kirje otsinguajaloost\? - Viimati esitatud - Enim esitatud Avalehe sisu Tühi leht Kioskivaade diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 03f2e4cf004..1a0215034f9 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -238,8 +238,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 0e2e72677a1..d926aecc4c5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -186,8 +186,6 @@ تاریخچه تاریخچه می‌خواهید این مورد را از تاریخچه جستجو پاک کنید؟ - آخرین پخش‌شده - بیشترین پخش‌شده محتوای صفحه اصلی صفحه خالی صفحه کیوسک diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 4f4833cce6c..545aa870164 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -241,8 +241,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 b5e746a8b52..7c454aebd8b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -221,8 +221,6 @@ Créer Rejeter Renommer - Dernière lecture - Vidéos les plus vues Toujours demander Nouvelle playlist Renommer diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index d6b92ac0c5c..23d7b8730e6 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -224,8 +224,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 9f7198dda32..267b07ac41d 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -288,8 +288,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 59e89fbe778..576a5454785 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -207,8 +207,6 @@ वापस दें वेबसाइट अधिक जानकारी और खबरों के लिए न्यूपाइप की वेबसाइट पर जाएं। - पिछला चलाया गया - अधिकतम चलाए गए निर्यात संपन्न हुआ आयात संपन्न हुआ कोई वैध ज़िप फ़ाइल नहीं है diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e3aab8e034b..a2d4a49954f 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -249,8 +249,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 46e24d8124f..729cfbbf9b7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -215,8 +215,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 Témagyűjtemények 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 f6c500bc41b..9a1cbe00dc4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -187,8 +187,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 kiosk diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 554ad989cb3..dd4c9b51f76 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -263,8 +263,6 @@ Ferill Ferill Lesa leyfi - Nýlega spilað - Mest spilað Aðalsíða Flutt út Flutt inn diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 83bd938bb4f..cd80f55ee12 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -222,8 +222,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 3952afb18bd..fb96bd6eb15 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -265,8 +265,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 72bb4a951a0..12220b6c305 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -314,8 +314,6 @@ ისტორია ისტორია გსურთ წაშალოთ ეს ელემენტი ძიების ისტორიიდან\? - ბოლოს დაუკრა - ხშირად დაკრული მთავარი გვერდის შინაარსი რა ჩანართებია ნაჩვენები მთავარ გვერდზე გადაფურცლეთ ელემენტები მათი ამოსაშლელად diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 29db7417227..3feea45728f 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -91,7 +91,6 @@ Ulac Yettwanɣel ɣef afus Tibdarin n tɣuri - Aneggaru yettwaslekmen Taɣuri tawurmant Aneqqis Sider @@ -212,7 +211,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 24bc5574e43..2f9b892d0ba 100644 --- a/app/src/main/res/values-kmr/strings.xml +++ b/app/src/main/res/values-kmr/strings.xml @@ -154,8 +154,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 ccd0b455afc..4c6bfdeab1e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -206,8 +206,6 @@ 취소 이름 바꾸기 reCAPTCHA 확인 - 마지막으로 재생 - 가장 많이 재생 내보내기 완료 가져오기 완료 유효한 ZIP 파일 없음 diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 3dc51fcc815..304f5445183 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -187,8 +187,6 @@ مێژوو مێژوو ئایا دەتەوێ ئەم بابەتە لە مێژووی گەڕان بسڕدرێتەوە؟ - دواین کارپێکراو - زۆرترین کارپێکراو ناوەڕۆکی پەڕەی سەرەکی لادان وردەکارییەکان diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index d0f37ad0071..ca07432b662 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -211,8 +211,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 b776d798968..17057c2ba13 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 3a8fa2f072f..4747f94bdd5 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -206,8 +206,6 @@ Историја Историја Сакаш да го избришеш предметот од историјата? - Последно пуштено - Најгледани Содржина Празна страна Киоск diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index be358bba0ec..def9fa15772 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -97,8 +97,6 @@ ശൂന്യമായ പേജ് പ്രധാന പേജിൽ കാണിക്കേണ്ട ടാബുകൾ പ്രധാന പേജ് ഉള്ളടക്കം - ഏറ്റവും കൂടുതൽ തവണ പ്ലേ ചെയ്തത് - അവസാനം പ്ലേ ചെയ്തത് സെർച്ച് ചരിത്രത്തിൽനിന്ന് ഈ item നീക്കം ചെയ്യട്ടെയോ\? ചരിത്രം ചരിത്രം diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index e70e61a74b8..b3c223722bd 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -232,8 +232,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 fa70a92f81c..0021fb17203 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -219,8 +219,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 b40145aa680..562972dd86d 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -239,8 +239,6 @@ इतिहास इतिहास तपाईं खोज इतिहासबाट यो वस्तु मेटाउन चाहनुहुन्छ\? - पछिल्लो पालि खोलिएको - धेरै हेरिएको मुख्य पृष्ठको सामग्री मुख्य पृष्ठ मा कुनकुन ट्याबहरू देखाइन्छ खाली पृष्ठ diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index c775ee17e2a..6f2f13168b3 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -207,8 +207,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 a257b3f2ec5..a38cfdcb213 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -219,8 +219,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 c83ced38ba9..ea496705abe 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -318,7 +318,6 @@ ߣߴߌ ߞߊ߬ ߜߟߍ߬ߦߊ߬ ߡߊߛߐ߬ߘߐ߲߬ ߟߥߊߟߌߟߊ߲ ߠߊߓߊ߯ߙߊ ߘߐ߫߸ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ߬ ߞߍߣߍ߲߫ ߛߌߦߊߡߊ߲ ߠߎ߬ ߡߊߝߍߣߍ߲߫ ߹ ߘߝߐ߬ߦߊ ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߝߌ߬ߛߌ ߣߌ߲߬ ߖߐ߬ߛߌ߫ ߢߌߣߌ߲ߠߌ߲߫ ߘߝߐ߬ߦߊ ߟߎ߬ ߘߐ߫؟ - ߦߋߡߍ߲ߕߊ߫ ߦߋߣߍ߲ߓߊ ߟߎ߬ ߓߏ߬ߟߏ߲߬ ߞߐߜߍ ߞߣߐߘߐ ߏ߬ ߘߴߌ ߟߊ߫ ߛߋ߲߬ߠߊ߬ ߢߊߓߐߟߌ ߟߎ߬ ߓߍ߯ ߝߌߘߊ߲߫. ߛߎߥߊ߲ߘߟߌ ߞߍ߫ ߛߏ߬ߙߌ߲߬ߘߐ ߟߎ߬ ߟߊ߫߸ ߡߍ߲ ߠߎ߫ ߦߌ߬ߘߊ߬ߕߐ߫ ߓߏ߬ߟߏ߲߬ ߞߐߜߍ ߞߊ߲߬ @@ -393,7 +392,6 @@ ߞߊ߬ ߟߊ߬ߘߌߢߍ ߞߊ߬ߙߊ߲߬ ߊ߬ ߡߊߝߍߣߍ߲߫ ߗߍߦߙߐ ߟߊ߫ ߘߝߐ߬ߦߊ - ߞߐߟߕߊ߫ ߕߏߟߏ߲ߣߍ߲ ߠߎ߬ ߥߙߏߝߋ߫ ߞߐߜߍ ߝߎ߲ߞߎ߲ߟߋ߲ ߥߙߏߝߋ ߘߏ߫ ߛߎߥߊ߲ߘߌ߫ diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bcb3d4ea418..4aa27b8f208 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -319,7 +319,6 @@ ଗୋପନୀୟତା ନୀତି ପଢ଼ନ୍ତୁ NewPipe ର ଲାଇସେନ୍ସ ଲାଇସେନ୍ସ ପଢ଼ନ୍ତୁ - ଶେଷ ଥର ପ୍ଲେ ହୋଇଛି ଖାଲି ପୃଷ୍ଠା ଚ୍ୟାନେଲ୍ ପୃଷ୍ଠା କିଓସ୍କ ପୃଷ୍ଠା @@ -623,7 +622,6 @@ ଯଦି ଆପଣ ଆପ୍ ବ୍ୟବହାର କରିବାରେ ଅସୁବିଧାର ସମ୍ମୁଖୀନ ହେଉଛନ୍ତି, ସାଧାରଣ ପ୍ରଶ୍ନର ଏହି ଉତ୍ତରଗୁଡିକ ଯାଞ୍ଚ କରିବାକୁ ନିଶ୍ଚିତ ହୁଅନ୍ତୁ! ୱେବସାଇଟ୍ ରେ ଦେଖନ୍ତୁ ଆପଣ ସନ୍ଧାନ ଇତିହାସରୁ ଏହି ଆଇଟମ୍ ବିଲୋପ କରିବାକୁ ଚାହୁଁଛନ୍ତି କି\? - ଅଧିକାଂଶ ପ୍ଲେ ହୋଇଛି ମୁଖ୍ୟ ପୃଷ୍ଠାରେ କେଉଁ ଟ୍ୟାବଗୁଡ଼ିକ ଦେଖାଯାଏ ଏକ ଚ୍ୟାନେଲ୍ ଚୟନ କରନ୍ତୁ ସେଗୁଡିକ ଅପସାରଣ କରିବା ପାଇଁ ଆଇଟମଗୁଡିକ ସ୍ୱାଇପ୍ କରନ୍ତୁ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 052ef0e7892..7e446bb4c6e 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -214,8 +214,6 @@ ਇਤਿਹਾਸ ਇਤਿਹਾਸ ਕੀ ਤੁਸੀਂ ਇਸਨੂੰ ਖੋਜ ਇਤਿਹਾਸ ਵਿੱਚੋਂ ਮਿਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ\? - ਆਖਰੀ ਚਲਾਈ ਗਈ - ਸਭ ਤੋਂ ਜਿਆਦਾ ਚਲਾਈ ਗਈ ਮੁੱਖ ਪੰਨੇ ਦੀ ਸਮੱਗਰੀ ਖ਼ਾਲੀ ਪੰਨਾ ਕਿਓਸਕ ਪੰਨਾ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 79f600b1aa0..e7e907e6be8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -218,8 +218,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 689e2d944ea..9821bad3c2f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -220,8 +220,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 1bdeaf39fe7..6ebed87c982 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -264,7 +264,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 @@ -315,7 +314,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 5918e408cb1..1691182cb6e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -227,8 +227,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 95492fed739..b0b0f4861eb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -214,8 +214,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 17c39ea57f0..a3ba90f1d4c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -239,8 +239,6 @@ Создать Скрыть Переименовать - Недавно проигранные - Часто проигрываемые Экспорт завершён Импорт завершён Нет верного Zip-файла diff --git a/app/src/main/res/values-ryu/strings.xml b/app/src/main/res/values-ryu/strings.xml index d6db9dd44b0..99f1472da61 100644 --- a/app/src/main/res/values-ryu/strings.xml +++ b/app/src/main/res/values-ryu/strings.xml @@ -267,8 +267,6 @@ NewPipeプロジェクトーうんじゅがプライバシーふぃじょうにてーしちなちょーいびーん。うんじゅがちゃーいがねーんかぎり、アプレーいかなるデータんしゅうしゅうさびらん。 \nNewPipeぬプライバシー・ポリシーっしぇー、クラッシュリポートそうしんじんかいちゃぬぐとーるしゅるいぬデータぬあんしん・きるくされいがしーょうさいにしちめいそーいびーん。 NewPipeーコピーレフトなソフトウェアやいびーん。うんじょーじゆうにうりさし、きんきゅうしー、きょうゆうし、あんしがいじんするくとぅがなやびーん。うんじょー、GNUフリーソフトウェアじぇーやんんがかんかいすん GNU General Publicライセンスバージョン3いかぬむとぅんかい、じゆうにさえーいーん・しゅうせいうくないるくとぅがなやびーん。 - さいしゅうさいせいにちじ - むっとぅむさいせいさったんちゃーしが かくだい プレイリスト 「ながうしっしキューんかいちちが」ぬヒントひょうじ @@ -278,7 +276,6 @@ ながうしっしキューんかいちちが ポップアップっしりんずくささるゆいかいし うくぬみぬ「ふぃらく」アクション - コンテンツふぃらちゅるとぅちぬデフォルトちゃーさ — %s フィット じんぬみん じどうせいせい @@ -526,7 +523,6 @@ せいけいじみリポートコピー プレイリストページ プレイリストさんたくちくぃみそーれー - じちゃーてぃきなさうぅいゆいかいしさびーん — %s じちゃーっしキューんかいちちが アクティブやるプレイヤーぬキューぬいりちがーやびーん プレイヤーびちぬプレイヤーんかいきりけーいねーキューぬうきかわいるかのうゆいがあいびーん diff --git a/app/src/main/res/values-sat/strings.xml b/app/src/main/res/values-sat/strings.xml index f9bf2177af4..13766b6cc3e 100644 --- a/app/src/main/res/values-sat/strings.xml +++ b/app/src/main/res/values-sat/strings.xml @@ -502,8 +502,6 @@ ᱱᱟᱜᱟᱢ ᱱᱟᱜᱟᱢ ᱟᱢ ᱱᱚᱶᱟ ᱡᱤᱱᱤᱥ ᱥᱟᱸᱪᱟᱨ ᱱᱟᱜᱟᱢ ᱠᱷᱚᱱ ᱵᱚᱫᱚᱞ ᱢᱮᱢᱮ? - ᱢᱩᱪᱟᱹᱫ ᱠᱷᱮᱞ ᱟᱠᱟᱱᱟ - ᱡᱟᱹᱥᱛᱤ ᱠᱷᱮᱞ ᱟᱠᱟᱱ ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ ᱨᱮᱱᱟᱜ ᱥᱟᱦᱴᱟ ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ ᱨᱮ ᱚᱠᱟ ᱛᱟᱵᱽ ᱠᱚ ᱵᱚᱫᱚᱞ ᱟᱠᱟᱱᱟ ᱡᱤᱱᱤᱥ ᱠᱚ ᱵᱟᱧᱪᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱥᱣᱟᱭᱯ ᱢᱮ diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 046001c6f1e..3334ae46cf8 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -82,8 +82,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 e3e08229b8e..8dd84058163 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -194,8 +194,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 stránka Kiosk diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 8b4ba8f7bc4..4647ae37c1a 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -255,8 +255,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 14668add52d..9953afdabcd 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -157,8 +157,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 4b9c2ac3664..071044d08eb 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -223,8 +223,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 ce5b87be834..63408f7e79a 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -376,8 +376,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 07716448970..4d09a977e74 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -241,8 +241,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 f1d388393c7..c0428fb0409 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -601,8 +601,6 @@ நியூபைப் என்பது நகலெடுக்கப்பட்ட லிப்ரே மென்பொருள்: நீங்கள் அதைப் பயன்படுத்தலாம், படிக்கலாம், பகிரலாம் மற்றும் மேம்படுத்தலாம். குறிப்பாக நீங்கள் இலவச மென்பொருள் அறக்கட்டளையால் வெளியிடப்பட்ட குனு பொது பொதுமக்கள் உரிமத்தின் விதிமுறைகளின் கீழ் மறுபகிர்வு மற்றும்/அல்லது மாற்றியமைக்கலாம், உரிமத்தின் பதிப்பு 3 அல்லது (உங்கள் விருப்பத்தில்) பின்னர் எந்தப் பதிப்பையும் மாற்றலாம். பயன்பாட்டைப் பயன்படுத்துவதில் சிக்கல் இருந்தால், பொதுவான கேள்விகளுக்கு இந்த பதில்களைப் பார்க்கவும்! இணையதளத்தில் காண்க - கடைசியாக விளையாடியது - அதிகம் விளையாடியது என்ன தாவல்கள் முதன்மையான பக்கத்தில் காட்டப்பட்டுள்ளன வெற்று பக்கம் கியோச்க் பக்கம் diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index a422dc99685..2176ff009cc 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -328,7 +328,6 @@ దీనికి ఈ అనుమతి అవసరం \nతేలియాడే పద్ధతిలో తెరవండి © %3$s కింద %2$s ద్వారా %1$s - చివరిగా ఆడింది చరిత్ర, సభ్యత్వాలు, ప్లేజాబితాలు మరియు అమరికలను ఎగుమతిచేయుము మీ ప్రస్తుత చరిత్ర, సభ్యత్వాలు, ప్లేజాబితాలు మరియు (ఐచ్ఛికంగా) సెట్టింగ్‌లను భర్తీ చేస్తుంది డాటాబేసుని దిగుమతిచేయుము @@ -349,7 +348,6 @@ తిరిగి ఇవ్వండి వెబ్సైట్ NewPipe యొక్క గోప్యతా విధానం - ఎక్కువగా ఆడినవి ప్రధాన పేజీలో ఏ ట్యాబ్‌లు చూపబడతాయి కియోస్క్ పేజీ డిఫాల్ట్ కియోస్క్ diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 01be8d1d20b..6e5e11265ca 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -219,8 +219,6 @@ ประวัติ ประวัติ คุณต้องการลบรายการนี้ออกจากประวัติการค้นหาหรือไม่\? - เล่นครั้งล่าสุด - เล่นมากที่สุด เนื้อหาของหน้าหลัก แท็บใดบ้างที่ต้องการให้แสดงบนหน้าหลัก หน้าว่าง diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3570e837a8a..69695699569 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -215,8 +215,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 43490c75167..1583c7cfde5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -211,8 +211,6 @@ Прочитати ліцензію Історія Видалити цей елемент з історії пошуку\? - Відтворювалося останнім - Відтворювалося найбільше Вміст на головній сторінці Порожня сторінка Сторінка кіоску diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index bb95e2811ba..341499096b9 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -204,8 +204,6 @@ سرگزشت سرگزشت کیا آپ اس آئٹم کو تلاش کی سرگزشت سے حذف کرنا چاہتے ہیں؟ - آخری چلائی گئی - سب سے زیادہ چلائی گئی مرکزی صفحہ کا مواد خالی صفحہ رجحان صفحہ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 08e29b155da..f460a73cd4e 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -222,8 +222,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 75ab714ce82..781c462e035 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -245,8 +245,6 @@ 此操作会覆盖当前设置。 显示信息 收藏 - 最近观看 - 最多观看 每次询问 新建播放列表 重命名 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index c634fb74e7d..adee2da1d56 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -317,8 +317,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 2ceb296b0e4..721220d865e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -213,8 +213,6 @@ 建立 退出 重新命名 - 上一次播放 - 最常播放 總是詢問 新的播放清單 重新命名 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c439f19e272..c2aa14b3cff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -218,6 +218,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 @@ -395,8 +396,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 @@ -871,6 +872,10 @@ SoundCloud has discontinued the original Top 50 charts. The corresponding tab has been removed from your main page. Next NewPipeExtractor is a library for extracting things from streaming sites. It is a core component of NewPipe, but could be used independently. + Sort by + Button to clear watch history + Thumbnail for playlist %1$s + Thumbnail for stream %1$s %d comment %d comments