() {
- @Override
- public void onSubscribe(Subscription s) {
- if (feedSubscriber != null) feedSubscriber.cancel();
- feedSubscriber = s;
-
- int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size();
- if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT;
-
- boolean hasToLoad = requestSize > 0;
- if (hasToLoad) {
- requestLoadedAtomic.set(infoListAdapter.getItemsList().size());
- requestFeed(requestSize);
- }
- isLoading.set(hasToLoad);
- }
-
- @Override
- public void onNext(SubscriptionEntity subscriptionEntity) {
- if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
- subscriptionService.getChannelInfo(subscriptionEntity)
- .observeOn(AndroidSchedulers.mainThread())
- .onErrorComplete(
- (@io.reactivex.annotations.NonNull Throwable throwable) ->
- FeedFragment.super.onError(throwable))
- .subscribe(
- getChannelInfoObserver(subscriptionEntity.getServiceId(),
- subscriptionEntity.getUrl()));
- } else {
- requestFeed(1);
- }
- }
-
- @Override
- public void onError(Throwable exception) {
- FeedFragment.this.onError(exception);
- }
-
- @Override
- public void onComplete() {
- if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called");
- }
- };
- }
-
- /**
- * On each request, a subscription item from the updated table is transformed
- * into a ChannelInfo, containing the latest streams from the channel.
- *
- * Currently, the feed uses the first into from the list of streams.
- *
- * If chosen feed already displayed, then we request another feed from another
- * subscription, until the subscription table runs out of new items.
- *
- * This Observer is self-contained and will close itself when complete. However, this
- * does not obey the fragment lifecycle and may continue running in the background
- * until it is complete. This is done due to RxJava2 no longer propagate errors once
- * an observer is unsubscribed while the thread process is still running.
- *
- * To solve the above issue, we can either set a global RxJava Error Handler, or
- * manage exceptions case by case. This should be done if the current implementation is
- * too costly when dealing with larger subscription sets.
- *
- * @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded.
- */
- private MaybeObserver getChannelInfoObserver(final int serviceId, final String url) {
- return new MaybeObserver() {
- private Disposable observer;
-
- @Override
- public void onSubscribe(Disposable d) {
- observer = d;
- compositeDisposable.add(d);
- isLoading.set(true);
- }
-
- // Called only when response is non-empty
- @Override
- public void onSuccess(final ChannelInfo channelInfo) {
- if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) {
- onDone();
- return;
- }
-
- final InfoItem item = channelInfo.getRelatedItems().get(0);
- // Keep requesting new items if the current one already exists
- boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
- if (!itemExists) {
- infoListAdapter.addInfoItem(item);
- //updateSubscription(channelInfo);
- } else {
- requestFeed(1);
- }
- onDone();
- }
-
- @Override
- public void onError(Throwable exception) {
- showSnackBarError(exception,
- UserAction.SUBSCRIPTION,
- NewPipe.getNameOfService(serviceId),
- url, 0);
- requestFeed(1);
- onDone();
- }
-
- // Called only when response is empty
- @Override
- public void onComplete() {
- onDone();
- }
-
- private void onDone() {
- if (observer.isDisposed()) {
- return;
- }
-
- itemsLoaded.add(serviceId + url);
- compositeDisposable.remove(observer);
-
- int loaded = requestLoadedAtomic.incrementAndGet();
- if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) {
- requestLoadedAtomic.set(0);
- isLoading.set(false);
- }
-
- if (itemsLoaded.size() == subscriptionPoolSize) {
- if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded");
- allItemsLoaded.set(true);
- showListFooter(false);
- isLoading.set(false);
- hideLoading();
- if (infoListAdapter.getItemsList().size() == 0) {
- showEmptyState();
- }
- }
- }
- };
- }
-
- @Override
- protected void loadMoreItems() {
- isLoading.set(true);
- delayHandler.removeCallbacksAndMessages(null);
- // Add a little of a delay when requesting more items because the cache is so fast,
- // that the view seems stuck to the user when he scroll to the bottom
- delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300);
- }
-
- @Override
- protected boolean hasMoreItems() {
- return !allItemsLoaded.get();
- }
-
- private final Handler delayHandler = new Handler();
-
- private void requestFeed(final int count) {
- if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]");
- if (feedSubscriber == null) return;
-
- isLoading.set(true);
- delayHandler.removeCallbacksAndMessages(null);
- feedSubscriber.request(count);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- private void resetFragment() {
- if (DEBUG) Log.d(TAG, "resetFragment() called");
- if (subscriptionObserver != null) subscriptionObserver.dispose();
- if (compositeDisposable != null) compositeDisposable.clear();
- if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
-
- delayHandler.removeCallbacksAndMessages(null);
- requestLoadedAtomic.set(0);
- allItemsLoaded.set(false);
- showListFooter(false);
- itemsLoaded.clear();
- }
-
- private void disposeEverything() {
- if (subscriptionObserver != null) subscriptionObserver.dispose();
- if (compositeDisposable != null) compositeDisposable.clear();
- if (feedSubscriber != null) feedSubscriber.cancel();
- delayHandler.removeCallbacksAndMessages(null);
- }
-
- private boolean doesItemExist(final List items, final InfoItem item) {
- for (final InfoItem existingItem : items) {
- if (existingItem.getInfoType() == item.getInfoType() &&
- existingItem.getServiceId() == item.getServiceId() &&
- existingItem.getName().equals(item.getName()) &&
- existingItem.getUrl().equals(item.getUrl())) return true;
- }
- return false;
- }
-
- private int howManyItemsToLoad() {
- int heightPixels = getResources().getDisplayMetrics().heightPixels;
- int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height);
-
- int items = itemHeightPixels > 0
- ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT
- : MIN_ITEMS_INITIAL_LOAD;
- return Math.max(MIN_ITEMS_INITIAL_LOAD, items);
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Fragment Error Handling
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void showError(String message, boolean showRetryButton) {
- resetFragment();
- super.showError(message, showRetryButton);
- }
-
- @Override
- protected boolean onError(Throwable exception) {
- if (super.onError(exception)) return true;
-
- int errorId = exception instanceof ExtractionException
- ? R.string.parsing_error
- : R.string.general_error;
- onUnrecoverableError(exception,
- UserAction.SOMETHING_ELSE,
- "none",
- "Requesting feed",
- errorId);
- return true;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
new file mode 100644
index 000000000..64020d14c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2019 Mauricio Colli
+ * FeedFragment.kt is part of NewPipe
+ *
+ * License: GPL-3.0+
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package org.schabi.newpipe.local.feed
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.*
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.preference.PreferenceManager
+import icepick.State
+import kotlinx.android.synthetic.main.error_retry.*
+import kotlinx.android.synthetic.main.fragment_feed.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.fragments.list.BaseListFragment
+import org.schabi.newpipe.local.feed.service.FeedLoadService
+import org.schabi.newpipe.report.UserAction
+import org.schabi.newpipe.util.AnimationUtils.animateView
+import org.schabi.newpipe.util.Localization
+import java.util.*
+
+class FeedFragment : BaseListFragment() {
+ private lateinit var viewModel: FeedViewModel
+ @State @JvmField var listState: Parcelable? = null
+
+ private var groupId = FeedGroupEntity.GROUP_ALL_ID
+ private var groupName = ""
+ private var oldestSubscriptionUpdate: Calendar? = null
+
+ init {
+ setHasOptionsMenu(true)
+ useDefaultStateSaving(false)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) ?: FeedGroupEntity.GROUP_ALL_ID
+ groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_feed, container, false)
+ }
+
+ override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(rootView, savedInstanceState)
+
+ viewModel = ViewModelProviders.of(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
+ viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
+ }
+
+ override fun onPause() {
+ super.onPause()
+ listState = items_list?.layoutManager?.onSaveInstanceState()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateRelativeTimeViews()
+ }
+
+ override fun setUserVisibleHint(isVisibleToUser: Boolean) {
+ super.setUserVisibleHint(isVisibleToUser)
+
+ if (!isVisibleToUser && view != null) {
+ updateRelativeTimeViews()
+ }
+ }
+
+ override fun initListeners() {
+ super.initListeners()
+ refresh_root_view.setOnClickListener {
+ triggerUpdate()
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Menu
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+ activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
+ activity.supportActionBar?.subtitle = groupName
+
+ inflater.inflate(R.menu.menu_feed_fragment, menu)
+
+ if (useAsFrontPage) {
+ menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.menu_item_feed_help) {
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
+ val enableDisableButtonText = when {
+ usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
+ else -> R.string.feed_use_dedicated_fetch_method_enable_button
+ }
+
+ AlertDialog.Builder(requireContext())
+ .setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
+ .setNeutralButton(enableDisableButtonText) { _, _ ->
+ sharedPreferences.edit()
+ .putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
+ .apply()
+ }
+ .setPositiveButton(android.R.string.ok, null)
+ .create()
+ .show()
+ return true
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ override fun onDestroyOptionsMenu() {
+ super.onDestroyOptionsMenu()
+ activity?.supportActionBar?.subtitle = null
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ activity?.supportActionBar?.subtitle = null
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Handling
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun showLoading() {
+ animateView(refresh_root_view, false, 0)
+ animateView(items_list, false, 0)
+
+ animateView(loading_progress_bar, true, 200)
+ animateView(loading_progress_text, true, 200)
+
+ empty_state_view?.let { animateView(it, false, 0) }
+ animateView(error_panel, false, 0)
+ }
+
+ override fun hideLoading() {
+ animateView(refresh_root_view, true, 200)
+ animateView(items_list, true, 300)
+
+ animateView(loading_progress_bar, false, 0)
+ animateView(loading_progress_text, false, 0)
+
+ empty_state_view?.let { animateView(it, false, 0) }
+ animateView(error_panel, false, 0)
+ }
+
+ override fun showEmptyState() {
+ animateView(refresh_root_view, true, 200)
+ animateView(items_list, false, 0)
+
+ animateView(loading_progress_bar, false, 0)
+ animateView(loading_progress_text, false, 0)
+
+ empty_state_view?.let { animateView(it, true, 800) }
+ animateView(error_panel, false, 0)
+ }
+
+ override fun showError(message: String, showRetryButton: Boolean) {
+ infoListAdapter.clearStreamItemList()
+ animateView(refresh_root_view, false, 120)
+ animateView(items_list, false, 120)
+
+ animateView(loading_progress_bar, false, 120)
+ animateView(loading_progress_text, false, 120)
+
+ error_message_view.text = message
+ animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0)
+ animateView(error_panel, true, 300)
+ }
+
+ override fun handleResult(result: FeedState) {
+ when (result) {
+ is FeedState.ProgressState -> handleProgressState(result)
+ is FeedState.LoadedState -> handleLoadedState(result)
+ is FeedState.ErrorState -> if (handleErrorState(result)) return
+ }
+
+ updateRefreshViewState()
+ }
+
+ private fun handleProgressState(progressState: FeedState.ProgressState) {
+ showLoading()
+
+ val isIndeterminate = progressState.currentProgress == -1 &&
+ progressState.maxProgress == -1
+
+ if (!isIndeterminate) {
+ loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
+ } else if (progressState.progressMessage > 0) {
+ loading_progress_text?.setText(progressState.progressMessage)
+ } else {
+ loading_progress_text?.text = "∞/∞"
+ }
+
+ loading_progress_bar.isIndeterminate = isIndeterminate ||
+ (progressState.maxProgress > 0 && progressState.currentProgress == 0)
+ loading_progress_bar.progress = progressState.currentProgress
+
+ loading_progress_bar.max = progressState.maxProgress
+ }
+
+ private fun handleLoadedState(loadedState: FeedState.LoadedState) {
+ infoListAdapter.setInfoItemList(loadedState.items)
+ listState?.run {
+ items_list.layoutManager?.onRestoreInstanceState(listState)
+ listState = null
+ }
+
+ oldestSubscriptionUpdate = loadedState.oldestUpdate
+
+ if (loadedState.notLoadedCount > 0) {
+ refresh_subtitle_text.visibility = View.VISIBLE
+ refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
+ } else {
+ refresh_subtitle_text.visibility = View.GONE
+ }
+
+ if (loadedState.itemsErrors.isNotEmpty()) {
+ showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED,
+ "none", "Loading feed", R.string.general_error)
+ }
+
+ if (loadedState.items.isEmpty()) {
+ showEmptyState()
+ } else {
+ hideLoading()
+ }
+ }
+
+
+ private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
+ hideLoading()
+ errorState.error?.let {
+ onError(errorState.error)
+ return true
+ }
+ return false
+ }
+
+ private fun updateRelativeTimeViews() {
+ updateRefreshViewState()
+ infoListAdapter.notifyDataSetChanged()
+ }
+
+ private fun updateRefreshViewState() {
+ val oldestSubscriptionUpdateText = when {
+ oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!)
+ else -> "—"
+ }
+
+ refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Load Service Handling
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun doInitialLoadLogic() {}
+ override fun reloadContent() = triggerUpdate()
+ override fun loadMoreItems() {}
+ override fun hasMoreItems() = false
+
+ private fun triggerUpdate() {
+ getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply {
+ putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
+ })
+ listState = null
+ }
+
+ override fun onError(exception: Throwable): Boolean {
+ if (super.onError(exception)) return true
+
+ if (useAsFrontPage) {
+ showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
+ return true
+ }
+
+ onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
+ return true
+ }
+
+ companion object {
+ const val KEY_GROUP_ID = "ARG_GROUP_ID"
+ const val KEY_GROUP_NAME = "ARG_GROUP_NAME"
+
+ @JvmStatic
+ fun newInstance(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupName: String? = null): FeedFragment {
+ val feedFragment = FeedFragment()
+
+ feedFragment.arguments = Bundle().apply {
+ putLong(KEY_GROUP_ID, groupId)
+ putString(KEY_GROUP_NAME, groupName)
+ }
+
+ return feedFragment
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt
new file mode 100644
index 000000000..c37d6a0b3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt
@@ -0,0 +1,24 @@
+package org.schabi.newpipe.local.feed
+
+import androidx.annotation.StringRes
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import java.util.*
+
+sealed class FeedState {
+ data class ProgressState(
+ val currentProgress: Int = -1,
+ val maxProgress: Int = -1,
+ @StringRes val progressMessage: Int = 0
+ ) : FeedState()
+
+ data class LoadedState(
+ val items: List,
+ val oldestUpdate: Calendar? = null,
+ val notLoadedCount: Long,
+ val itemsErrors: List = emptyList()
+ ) : FeedState()
+
+ data class ErrorState(
+ val error: Throwable? = null
+ ) : FeedState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
new file mode 100644
index 000000000..adc262ecb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
@@ -0,0 +1,71 @@
+package org.schabi.newpipe.local.feed
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import io.reactivex.Flowable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.functions.Function4
+import io.reactivex.schedulers.Schedulers
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.local.feed.service.FeedEventManager
+import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.*
+import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
+ class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return FeedViewModel(context.applicationContext, groupId) as T
+ }
+ }
+
+ private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
+
+ private val mutableStateLiveData = MutableLiveData()
+ val stateLiveData: LiveData = mutableStateLiveData
+
+ private var combineDisposable = Flowable
+ .combineLatest(
+ FeedEventManager.events(),
+ feedDatabaseManager.asStreamItems(groupId),
+ feedDatabaseManager.notLoadedCount(groupId),
+ feedDatabaseManager.oldestSubscriptionUpdate(groupId),
+
+ Function4 { t1: FeedEventManager.Event, t2: List, t3: Long, t4: List ->
+ return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
+ }
+ )
+ .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ val (event, listFromDB, notLoadedCount, oldestUpdate) = it
+
+ val oldestUpdateCalendar =
+ oldestUpdate?.let { Calendar.getInstance().apply { time = it } }
+
+ mutableStateLiveData.postValue(when (event) {
+ is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount)
+ is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
+ is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors)
+ is ErrorResultEvent -> FeedState.ErrorState(event.error)
+ })
+
+ if (event is ErrorResultEvent || event is SuccessResultEvent) {
+ FeedEventManager.reset()
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ combineDisposable.dispose()
+ }
+
+ private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: Date?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt
new file mode 100644
index 000000000..e9012ff37
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt
@@ -0,0 +1,38 @@
+package org.schabi.newpipe.local.feed.service
+
+import androidx.annotation.StringRes
+import io.reactivex.Flowable
+import io.reactivex.processors.BehaviorProcessor
+import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
+import java.util.concurrent.atomic.AtomicBoolean
+
+object FeedEventManager {
+ private var processor: BehaviorProcessor = BehaviorProcessor.create()
+ private var ignoreUpstream = AtomicBoolean()
+ private var eventsFlowable = processor.startWith(IdleEvent)
+
+ fun postEvent(event: Event) {
+ processor.onNext(event)
+ }
+
+ fun events(): Flowable {
+ return eventsFlowable.filter { !ignoreUpstream.get() }
+ }
+
+ fun reset() {
+ ignoreUpstream.set(true)
+ postEvent(IdleEvent)
+ ignoreUpstream.set(false)
+ }
+
+ sealed class Event {
+ object IdleEvent : Event()
+ data class ProgressEvent(val currentProgress: Int = -1, val maxProgress: Int = -1, @StringRes val progressMessage: Int = 0) : Event() {
+ constructor(@StringRes progressMessage: Int) : this(-1, -1, progressMessage)
+ }
+
+ data class SuccessResultEvent(val itemsErrors: List = emptyList()) : Event()
+ data class ErrorResultEvent(val error: Throwable) : Event()
+ }
+
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
new file mode 100644
index 000000000..294a7fcd5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2019 Mauricio Colli
+ * FeedLoadService.kt is part of NewPipe
+ *
+ * License: GPL-3.0+
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package org.schabi.newpipe.local.feed.service
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.os.IBinder
+import android.preference.PreferenceManager
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import io.reactivex.Flowable
+import io.reactivex.Notification
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.functions.Consumer
+import io.reactivex.functions.Function
+import io.reactivex.processors.PublishProcessor
+import io.reactivex.schedulers.Schedulers
+import org.reactivestreams.Subscriber
+import org.reactivestreams.Subscription
+import org.schabi.newpipe.MainActivity.DEBUG
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.extractor.ListInfo
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.local.feed.FeedDatabaseManager
+import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.*
+import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
+import org.schabi.newpipe.local.subscription.SubscriptionManager
+import org.schabi.newpipe.util.ExtractorHelper
+import java.io.IOException
+import java.util.*
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.collections.ArrayList
+
+class FeedLoadService : Service() {
+ companion object {
+ private val TAG = FeedLoadService::class.java.simpleName
+ private const val NOTIFICATION_ID = 7293450
+ private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL"
+
+ /**
+ * How often the notification will be updated.
+ */
+ private const val NOTIFICATION_SAMPLING_PERIOD = 1500
+
+ /**
+ * How many extractions will be running in parallel.
+ */
+ private const val PARALLEL_EXTRACTIONS = 6
+
+ /**
+ * Number of items to buffer to mass-insert in the database.
+ */
+ private const val BUFFER_COUNT_BEFORE_INSERT = 20
+
+ const val EXTRA_GROUP_ID: String = "FeedLoadService.EXTRA_GROUP_ID"
+ }
+
+ private var loadingSubscription: Subscription? = null
+ private lateinit var subscriptionManager: SubscriptionManager
+
+ private lateinit var feedDatabaseManager: FeedDatabaseManager
+ private lateinit var feedResultsHolder: ResultsHolder
+
+ private var disposables = CompositeDisposable()
+ private var notificationUpdater = PublishProcessor.create()
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Lifecycle
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun onCreate() {
+ super.onCreate()
+ subscriptionManager = SubscriptionManager(this)
+ feedDatabaseManager = FeedDatabaseManager(this)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (DEBUG) {
+ Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," +
+ " flags = [" + flags + "], startId = [" + startId + "]")
+ }
+
+ if (intent == null || loadingSubscription != null) {
+ return START_NOT_STICKY
+ }
+
+ setupNotification()
+ setupBroadcastReceiver()
+ val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
+
+ val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
+ val useFeedExtractor = defaultSharedPreferences
+ .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
+
+ val thresholdOutdatedSecondsString = defaultSharedPreferences
+ .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value))
+ val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt()
+
+ startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds)
+
+ return START_NOT_STICKY
+ }
+
+ private fun disposeAll() {
+ unregisterReceiver(broadcastReceiver)
+
+ loadingSubscription?.cancel()
+ loadingSubscription = null
+
+ disposables.dispose()
+ }
+
+ private fun stopService() {
+ disposeAll()
+ stopForeground(true)
+ notificationManager.cancel(NOTIFICATION_ID)
+ stopSelf()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Loading & Handling
+ ///////////////////////////////////////////////////////////////////////////
+
+ private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
+ companion object {
+ fun wrapList(subscriptionId: Long, info: ListInfo): List {
+ val toReturn = ArrayList(info.errors.size)
+ for (error in info.errors) {
+ toReturn.add(RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, error))
+ }
+ return toReturn
+ }
+ }
+ }
+
+ private fun startLoading(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, useFeedExtractor: Boolean, thresholdOutdatedSeconds: Int) {
+ feedResultsHolder = ResultsHolder()
+
+ val outdatedThreshold = Calendar.getInstance().apply {
+ add(Calendar.SECOND, -thresholdOutdatedSeconds)
+ }.time
+
+ val subscriptions = when (groupId) {
+ FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
+ else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
+ }
+
+ subscriptions
+ .limit(1)
+
+ .doOnNext {
+ currentProgress.set(0)
+ maxProgress.set(it.size)
+ }
+ .filter { it.isNotEmpty() }
+
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnNext {
+ startForeground(NOTIFICATION_ID, notificationBuilder.build())
+ updateNotificationProgress(null)
+ broadcastProgress()
+ }
+
+ .observeOn(Schedulers.io())
+ .flatMap { Flowable.fromIterable(it) }
+ .takeWhile { !cancelSignal.get() }
+
+ .parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2)
+ .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
+ .filter { !cancelSignal.get() }
+
+ .map { subscriptionEntity ->
+ try {
+ val listInfo = if (useFeedExtractor) {
+ ExtractorHelper
+ .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
+ .blockingGet()
+ } else {
+ ExtractorHelper
+ .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
+ .blockingGet()
+ } as ListInfo
+
+ return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
+ } catch (e: Throwable) {
+ val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
+ val wrapper = RequestException(subscriptionEntity.uid, request, e)
+ return@map Notification.createOnError>>(wrapper)
+ }
+ }
+ .sequential()
+
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnNext(errorHandlingConsumer)
+
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnNext(notificationsConsumer)
+
+ .observeOn(Schedulers.io())
+ .buffer(BUFFER_COUNT_BEFORE_INSERT)
+ .doOnNext(databaseConsumer)
+
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(resultSubscriber)
+ }
+
+ private fun broadcastProgress() {
+ postEvent(ProgressEvent(currentProgress.get(), maxProgress.get()))
+ }
+
+ private val resultSubscriber
+ get() = object : Subscriber>>>> {
+
+ override fun onSubscribe(s: Subscription) {
+ loadingSubscription = s
+ s.request(java.lang.Long.MAX_VALUE)
+ }
+
+ override fun onNext(notification: List>>>) {
+ if (DEBUG) Log.v(TAG, "onNext() → $notification")
+ }
+
+ override fun onError(error: Throwable) {
+ handleError(error)
+ }
+
+ override fun onComplete() {
+ if (maxProgress.get() == 0) {
+ postEvent(IdleEvent)
+ stopService()
+
+ return
+ }
+
+ currentProgress.set(-1)
+ maxProgress.set(-1)
+
+ notificationUpdater.onNext(getString(R.string.feed_processing_message))
+ postEvent(ProgressEvent(R.string.feed_processing_message))
+
+ disposables.add(Single
+ .fromCallable {
+ feedResultsHolder.ready()
+
+ postEvent(ProgressEvent(R.string.feed_processing_message))
+ feedDatabaseManager.removeOrphansOrOlderStreams()
+
+ postEvent(SuccessResultEvent(feedResultsHolder.itemsErrors))
+ true
+ }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe { _, throwable ->
+ if (throwable != null) {
+ Log.e(TAG, "Error while storing result", throwable)
+ handleError(throwable)
+ return@subscribe
+ }
+ stopService()
+ })
+ }
+ }
+
+ private val databaseConsumer: Consumer>>>>
+ get() = Consumer {
+ feedDatabaseManager.database().runInTransaction {
+ for (notification in it) {
+
+ if (notification.isOnNext) {
+ val subscriptionId = notification.value!!.first
+ val info = notification.value!!.second
+
+ feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
+ subscriptionManager.updateFromInfo(subscriptionId, info)
+
+ if (info.errors.isNotEmpty()) {
+ feedResultsHolder.addErrors(RequestException.wrapList(subscriptionId, info))
+ feedDatabaseManager.markAsOutdated(subscriptionId)
+ }
+
+ } else if (notification.isOnError) {
+ val error = notification.error!!
+ feedResultsHolder.addError(error)
+
+ if (error is RequestException) {
+ feedDatabaseManager.markAsOutdated(error.subscriptionId)
+ }
+ }
+ }
+ }
+ }
+
+
+ private val errorHandlingConsumer: Consumer>>>
+ get() = Consumer {
+ if (it.isOnError) {
+ var error = it.error!!
+ if (error is RequestException) error = error.cause!!
+ val cause = error.cause
+
+ when {
+ error is IOException -> throw error
+ cause is IOException -> throw cause
+
+ error is ReCaptchaException -> throw error
+ cause is ReCaptchaException -> throw cause
+ }
+ }
+ }
+
+ private val notificationsConsumer: Consumer>>>
+ get() = Consumer { onItemCompleted(it.value?.second?.name) }
+
+ private fun onItemCompleted(updateDescription: String?) {
+ currentProgress.incrementAndGet()
+ notificationUpdater.onNext(updateDescription ?: "")
+
+ broadcastProgress()
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Notification
+ ///////////////////////////////////////////////////////////////////////////
+
+ private lateinit var notificationManager: NotificationManagerCompat
+ private lateinit var notificationBuilder: NotificationCompat.Builder
+
+ private var currentProgress = AtomicInteger(-1)
+ private var maxProgress = AtomicInteger(-1)
+
+ private fun createNotification(): NotificationCompat.Builder {
+ val cancelActionIntent = PendingIntent.getBroadcast(this,
+ NOTIFICATION_ID, Intent(ACTION_CANCEL), 0)
+
+ return NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
+ .setOngoing(true)
+ .setProgress(-1, -1, true)
+ .setSmallIcon(R.drawable.ic_newpipe_triangle_white)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .addAction(0, getString(R.string.cancel), cancelActionIntent)
+ .setContentTitle(getString(R.string.feed_notification_loading))
+ }
+
+ private fun setupNotification() {
+ notificationManager = NotificationManagerCompat.from(this)
+ notificationBuilder = createNotification()
+
+ val throttleAfterFirstEmission = Function { flow: Flowable ->
+ flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS))
+ }
+
+ disposables.add(notificationUpdater
+ .publish(throttleAfterFirstEmission)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(this::updateNotificationProgress))
+ }
+
+ private fun updateNotificationProgress(updateDescription: String?) {
+ notificationBuilder.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1)
+
+ if (maxProgress.get() == -1) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) notificationBuilder.setContentInfo(null)
+ if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription)
+ notificationBuilder.setContentText(updateDescription)
+ } else {
+ val progressText = this.currentProgress.toString() + "/" + maxProgress
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText("$updateDescription ($progressText)")
+ } else {
+ notificationBuilder.setContentInfo(progressText)
+ if (!updateDescription.isNullOrEmpty()) notificationBuilder.setContentText(updateDescription)
+ }
+ }
+
+ notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Notification Actions
+ ///////////////////////////////////////////////////////////////////////////
+
+ private lateinit var broadcastReceiver: BroadcastReceiver
+ private val cancelSignal = AtomicBoolean()
+
+ private fun setupBroadcastReceiver() {
+ broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == ACTION_CANCEL) {
+ cancelSignal.set(true)
+ }
+ }
+ }
+ registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL))
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Error handling
+ ///////////////////////////////////////////////////////////////////////////
+
+ private fun handleError(error: Throwable) {
+ postEvent(ErrorResultEvent(error))
+ stopService()
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Results Holder
+ ///////////////////////////////////////////////////////////////////////////
+
+ class ResultsHolder {
+ /**
+ * List of errors that may have happen during loading.
+ */
+ internal lateinit var itemsErrors: List
+
+ private val itemsErrorsHolder: MutableList = ArrayList()
+
+ fun addError(error: Throwable) {
+ itemsErrorsHolder.add(error)
+ }
+
+ fun addErrors(errors: List) {
+ itemsErrorsHolder.addAll(errors)
+ }
+
+ fun ready() {
+ itemsErrors = itemsErrorsHolder.toList()
+ }
+ }
+}
\ No newline at end of file
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 d84fe0195..d208f92b3 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
@@ -269,11 +269,11 @@ public class HistoryRecordManager {
for (LocalItem item : items) {
long streamId;
if (item instanceof StreamStatisticsEntry) {
- streamId = ((StreamStatisticsEntry) item).streamId;
+ streamId = ((StreamStatisticsEntry) item).getStreamId();
} else if (item instanceof PlaylistStreamEntity) {
streamId = ((PlaylistStreamEntity) item).getStreamUid();
} else if (item instanceof PlaylistStreamEntry) {
- streamId = ((PlaylistStreamEntry) item).streamId;
+ streamId = ((PlaylistStreamEntry) item).getStreamId();
} else {
result.add(null);
continue;
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
index 31ae70954..a54c2a9a4 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
@@ -76,11 +76,11 @@ public class StatisticsPlaylistFragment
switch (sortMode) {
case LAST_PLAYED:
Collections.sort(results, (left, right) ->
- right.latestAccessDate.compareTo(left.latestAccessDate));
+ right.getLatestAccessDate().compareTo(left.getLatestAccessDate()));
return results;
case MOST_PLAYED:
Collections.sort(results, (left, right) ->
- Long.compare(right.watchCount, left.watchCount));
+ Long.compare(right.getWatchCount(), left.getWatchCount()));
return results;
default: return null;
}
@@ -153,9 +153,9 @@ public class StatisticsPlaylistFragment
if (selectedItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
NavigationHelper.openVideoDetailFragment(getFM(),
- item.serviceId,
- item.url,
- item.title);
+ item.getStreamEntity().getServiceId(),
+ item.getStreamEntity().getUrl(),
+ item.getStreamEntity().getTitle());
}
}
@@ -402,7 +402,7 @@ public class StatisticsPlaylistFragment
.get(index);
if(infoItem instanceof StreamStatisticsEntry) {
final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem;
- final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId)
+ final Disposable onDelete = recordManager.deleteStreamHistory(entry.getStreamId())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> {
diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java
index 30cc6de32..7eef3e67e 100644
--- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java
@@ -52,12 +52,12 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
if (!(localItem instanceof PlaylistStreamEntry)) return;
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
- itemVideoTitleView.setText(item.title);
- itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader,
- NewPipe.getNameOfService(item.serviceId)));
+ itemVideoTitleView.setText(item.getStreamEntity().getTitle());
+ itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getStreamEntity().getUploader(),
+ NewPipe.getNameOfService(item.getStreamEntity().getServiceId())));
- if (item.duration > 0) {
- itemDurationView.setText(Localization.getDurationString(item.duration));
+ 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);
@@ -65,7 +65,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE);
- itemProgressView.setMax((int) item.duration);
+ itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
@@ -75,7 +75,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
- itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
+ itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
@@ -102,8 +102,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0);
- if (state != null && item.duration > 0) {
- itemProgressView.setMax((int) item.duration);
+ if (state != null && item.getStreamEntity().getDuration() > 0) {
+ itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
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
index 75fbf13ea..77f947031 100644
--- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java
@@ -71,9 +71,9 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
final DateFormat dateFormat) {
final String watchCount = Localization.shortViewCount(itemBuilder.getContext(),
- entry.watchCount);
- final String uploadDate = dateFormat.format(entry.latestAccessDate);
- final String serviceName = NewPipe.getNameOfService(entry.serviceId);
+ entry.getWatchCount());
+ final String uploadDate = dateFormat.format(entry.getLatestAccessDate());
+ final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId());
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
}
@@ -82,11 +82,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
if (!(localItem instanceof StreamStatisticsEntry)) return;
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
- itemVideoTitleView.setText(item.title);
- itemUploaderView.setText(item.uploader);
+ itemVideoTitleView.setText(item.getStreamEntity().getTitle());
+ itemUploaderView.setText(item.getStreamEntity().getUploader());
- if (item.duration > 0) {
- itemDurationView.setText(Localization.getDurationString(item.duration));
+ 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);
@@ -94,7 +94,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0);
if (state != null) {
itemProgressView.setVisibility(View.VISIBLE);
- itemProgressView.setMax((int) item.duration);
+ itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
itemProgressView.setVisibility(View.GONE);
@@ -109,7 +109,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
- itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
+ itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
@@ -133,8 +133,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
StreamStateEntity state = historyRecordManager.loadLocalStreamStateBatch(new ArrayList() {{ add(localItem); }}).blockingGet().get(0);
- if (state != null && item.duration > 0) {
- itemProgressView.setMax((int) item.duration);
+ if (state != null && item.getStreamEntity().getDuration() > 0) {
+ itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()));
} else {
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index 17599a1ca..dd9958486 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -168,7 +168,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment streamIds = new ArrayList<>(items.size());
for (final LocalItem item : items) {
if (item instanceof PlaylistStreamEntry) {
- streamIds.add(((PlaylistStreamEntry) item).streamId);
+ streamIds.add(((PlaylistStreamEntry) item).getStreamId());
}
}
@@ -579,7 +579,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(item), true));
StreamDialogEntry.set_as_playlist_thumbnail.setCustomAction(
- (fragment, infoItemDuplicate) -> changeThumbnailUrl(item.thumbnailUrl));
+ (fragment, infoItemDuplicate) -> changeThumbnailUrl(item.getStreamEntity().getThumbnailUrl()));
StreamDialogEntry.delete.setCustomAction(
(fragment, infoItemDuplicate) -> deleteItem(item));
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt
new file mode 100644
index 000000000..9ff08c32c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/FeedGroupIcon.kt
@@ -0,0 +1,63 @@
+package org.schabi.newpipe.local.subscription
+
+import android.content.Context
+import androidx.annotation.AttrRes
+import androidx.annotation.DrawableRes
+import org.schabi.newpipe.R
+import org.schabi.newpipe.util.ThemeHelper
+
+enum class FeedGroupIcon(
+ /**
+ * The id that will be used to store and retrieve icons from some persistent storage (e.g. DB).
+ */
+ val id: Int,
+
+ /**
+ * The attribute that points to a drawable resource. "R.attr" is used here to support multiple themes.
+ */
+ @AttrRes val drawableResourceAttr: Int
+) {
+ ALL(0, R.attr.ic_asterisk),
+ MUSIC(1, R.attr.ic_music_note),
+ EDUCATION(2, R.attr.ic_school),
+ FITNESS(3, R.attr.ic_fitness),
+ SPACE(4, R.attr.ic_telescope),
+ COMPUTER(5, R.attr.ic_computer),
+ GAMING(6, R.attr.ic_videogame),
+ SPORTS(7, R.attr.ic_sports),
+ NEWS(8, R.attr.ic_megaphone),
+ FAVORITES(9, R.attr.ic_heart),
+ CAR(10, R.attr.ic_car),
+ MOTORCYCLE(11, R.attr.ic_motorcycle),
+ TREND(12, R.attr.ic_trending_up),
+ MOVIE(13, R.attr.ic_movie),
+ BACKUP(14, R.attr.ic_backup),
+ ART(15, R.attr.palette),
+ PERSON(16, R.attr.ic_person),
+ PEOPLE(17, R.attr.ic_people),
+ MONEY(18, R.attr.ic_money),
+ KIDS(19, R.attr.ic_kids),
+ FOOD(20, R.attr.ic_fastfood),
+ SMILE(21, R.attr.ic_smile),
+ EXPLORE(22, R.attr.ic_explore),
+ RESTAURANT(23, R.attr.ic_restaurant),
+ MIC(24, R.attr.ic_mic),
+ HEADSET(25, R.attr.audio),
+ RADIO(26, R.attr.ic_radio),
+ SHOPPING_CART(27, R.attr.ic_shopping_cart),
+ WATCH_LATER(28, R.attr.ic_watch_later),
+ WORK(29, R.attr.ic_work),
+ HOT(30, R.attr.ic_hot),
+ CHANNEL(31, R.attr.ic_channel),
+ BOOKMARK(32, R.attr.ic_bookmark),
+ PETS(33, R.attr.ic_pets),
+ WORLD(34, R.attr.ic_world),
+ STAR(35, R.attr.ic_stars),
+ SUN(36, R.attr.ic_sunny),
+ RSS(37, R.attr.rss);
+
+ @DrawableRes
+ fun getDrawableRes(context: Context): Int {
+ return ThemeHelper.resolveResourceIdFromAttr(context, drawableResourceAttr)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java
deleted file mode 100644
index bff6c1b3a..000000000
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java
+++ /dev/null
@@ -1,595 +0,0 @@
-package org.schabi.newpipe.local.subscription;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Parcelable;
-import android.preference.PreferenceManager;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentManager;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import androidx.appcompat.app.ActionBar;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.nononsenseapps.filepicker.Utils;
-
-import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.subscription.SubscriptionEntity;
-import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.StreamingService;
-import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
-import org.schabi.newpipe.extractor.exceptions.ExtractionException;
-import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
-import org.schabi.newpipe.fragments.BaseStateFragment;
-import org.schabi.newpipe.info_list.InfoListAdapter;
-import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
-import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
-import org.schabi.newpipe.report.UserAction;
-import org.schabi.newpipe.util.FilePickerActivityHelper;
-import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.OnClickGesture;
-import org.schabi.newpipe.util.ServiceHelper;
-import org.schabi.newpipe.util.ShareUtils;
-import org.schabi.newpipe.util.ThemeHelper;
-import org.schabi.newpipe.views.CollapsibleView;
-
-import java.io.File;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-import icepick.State;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-
-import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
-import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
-import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE;
-import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
-import static org.schabi.newpipe.util.AnimationUtils.animateView;
-
-public class SubscriptionFragment extends BaseStateFragment> implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final int REQUEST_EXPORT_CODE = 666;
- private static final int REQUEST_IMPORT_CODE = 667;
-
- private RecyclerView itemsList;
- @State
- protected Parcelable itemsListState;
- private InfoListAdapter infoListAdapter;
- private int updateFlags = 0;
-
- private static final int LIST_MODE_UPDATE_FLAG = 0x32;
-
- private View whatsNewItemListHeader;
- private View importExportListHeader;
-
- @State
- protected Parcelable importExportOptionsState;
- private CollapsibleView importExportOptions;
-
- private CompositeDisposable disposables = new CompositeDisposable();
- private SubscriptionService subscriptionService;
-
- ///////////////////////////////////////////////////////////////////////////
- // Fragment LifeCycle
- ///////////////////////////////////////////////////////////////////////////
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setHasOptionsMenu(true);
- PreferenceManager.getDefaultSharedPreferences(activity)
- .registerOnSharedPreferenceChangeListener(this);
- }
-
- @Override
- public void setUserVisibleHint(boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (activity != null && isVisibleToUser) {
- setTitle(activity.getString(R.string.tab_subscriptions));
- }
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- infoListAdapter = new InfoListAdapter(activity);
- subscriptionService = SubscriptionService.getInstance(activity);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_subscription, container, false);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- setupBroadcastReceiver();
- if (updateFlags != 0) {
- if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
- final boolean useGrid = isGridLayout();
- itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
- infoListAdapter.setGridItemVariants(useGrid);
- infoListAdapter.notifyDataSetChanged();
- }
- updateFlags = 0;
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
- importExportOptionsState = importExportOptions.onSaveInstanceState();
-
- if (subscriptionBroadcastReceiver != null && activity != null) {
- LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
- }
- }
-
- @Override
- public void onDestroyView() {
- if (disposables != null) disposables.clear();
-
- super.onDestroyView();
- }
-
- @Override
- public void onDestroy() {
- if (disposables != null) disposables.dispose();
- disposables = null;
- subscriptionService = null;
-
- PreferenceManager.getDefaultSharedPreferences(activity)
- .unregisterOnSharedPreferenceChangeListener(this);
- super.onDestroy();
- }
-
- protected RecyclerView.LayoutManager getListLayoutManager() {
- return new LinearLayoutManager(activity);
- }
-
- protected RecyclerView.LayoutManager getGridLayoutManager() {
- final Resources resources = activity.getResources();
- int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
- width += (24 * resources.getDisplayMetrics().density);
- final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
- final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
- lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
- return lm;
- }
-
- /*/////////////////////////////////////////////////////////////////////////
- // Menu
- /////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
-
- ActionBar supportActionBar = activity.getSupportActionBar();
- if (supportActionBar != null) {
- supportActionBar.setDisplayShowTitleEnabled(true);
- setTitle(getString(R.string.tab_subscriptions));
- }
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Subscriptions import/export
- //////////////////////////////////////////////////////////////////////////*/
-
- private BroadcastReceiver subscriptionBroadcastReceiver;
-
- private void setupBroadcastReceiver() {
- if (activity == null) return;
-
- if (subscriptionBroadcastReceiver != null) {
- LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
- }
-
- final IntentFilter filters = new IntentFilter();
- filters.addAction(SubscriptionsExportService.EXPORT_COMPLETE_ACTION);
- filters.addAction(SubscriptionsImportService.IMPORT_COMPLETE_ACTION);
- subscriptionBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (importExportOptions != null) importExportOptions.collapse();
- }
- };
-
- LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver, filters);
- }
-
- private View addItemView(final String title, @DrawableRes final int icon, ViewGroup container) {
- final View itemRoot = View.inflate(getContext(), R.layout.subscription_import_export_item, null);
- final TextView titleView = itemRoot.findViewById(android.R.id.text1);
- final ImageView iconView = itemRoot.findViewById(android.R.id.icon1);
-
- titleView.setText(title);
- iconView.setImageResource(icon);
-
- container.addView(itemRoot);
- return itemRoot;
- }
-
- private void setupImportFromItems(final ViewGroup listHolder) {
- final View previousBackupItem = addItemView(getString(R.string.previous_export),
- ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder);
- previousBackupItem.setOnClickListener(item -> onImportPreviousSelected());
-
- final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE;
- final String[] services = getResources().getStringArray(R.array.service_list);
- for (String serviceName : services) {
- try {
- final StreamingService service = NewPipe.getService(serviceName);
-
- final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor();
- if (subscriptionExtractor == null) continue;
-
- final List supportedSources = subscriptionExtractor.getSupportedSources();
- if (supportedSources.isEmpty()) continue;
-
- final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder);
- final ImageView iconView = itemView.findViewById(android.R.id.icon1);
- iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
-
- itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId()));
- } catch (ExtractionException e) {
- throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e);
- }
- }
- }
-
- private void setupExportToItems(final ViewGroup listHolder) {
- final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder);
- previousBackupItem.setOnClickListener(item -> onExportSelected());
- }
-
- private void onImportFromServiceSelected(int serviceId) {
- FragmentManager fragmentManager = getFM();
- NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId);
- }
-
- private void onImportPreviousSelected() {
- startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE);
- }
-
- private void onExportSelected() {
- final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date());
- final String exportName = "newpipe_subscriptions_" + date + ".json";
- final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName);
-
- startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE);
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) {
- if (requestCode == REQUEST_EXPORT_CODE) {
- final File exportFile = Utils.getFileForUri(data.getData());
- if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) {
- Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show();
- } else {
- activity.startService(new Intent(activity, SubscriptionsExportService.class)
- .putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath()));
- }
- } else if (requestCode == REQUEST_IMPORT_CODE) {
- final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
- ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
- .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
- .putExtra(KEY_VALUE, path));
- }
- }
- }
- /*/////////////////////////////////////////////////////////////////////////
- // Fragment Views
- /////////////////////////////////////////////////////////////////////////*/
-
- @Override
- protected void initViews(View rootView, Bundle savedInstanceState) {
- super.initViews(rootView, savedInstanceState);
-
- final boolean useGrid = isGridLayout();
- infoListAdapter = new InfoListAdapter(getActivity());
- itemsList = rootView.findViewById(R.id.items_list);
- itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
-
- View headerRootLayout;
- infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
- whatsNewItemListHeader = headerRootLayout.findViewById(R.id.whats_new);
- importExportListHeader = headerRootLayout.findViewById(R.id.import_export);
- importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
-
- infoListAdapter.useMiniItemVariants(true);
- infoListAdapter.setGridItemVariants(useGrid);
- itemsList.setAdapter(infoListAdapter);
-
- setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
- setupExportToItems(headerRootLayout.findViewById(R.id.export_to_options));
-
- if (importExportOptionsState != null) {
- importExportOptions.onRestoreInstanceState(importExportOptionsState);
- importExportOptionsState = null;
- }
-
- importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon)));
- importExportOptions.ready();
- }
-
- private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) {
- return newState -> animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180);
- }
-
- @Override
- protected void initListeners() {
- super.initListeners();
-
- infoListAdapter.setOnChannelSelectedListener(new OnClickGesture() {
-
- public void selected(ChannelInfoItem selectedItem) {
- final FragmentManager fragmentManager = getFM();
- NavigationHelper.openChannelFragment(fragmentManager,
- selectedItem.getServiceId(),
- selectedItem.getUrl(),
- selectedItem.getName());
- }
-
- public void held(ChannelInfoItem selectedItem) {
- showLongTapDialog(selectedItem);
- }
-
- });
-
- whatsNewItemListHeader.setOnClickListener(v -> {
- FragmentManager fragmentManager = getFM();
- NavigationHelper.openWhatsNewFragment(fragmentManager);
- });
- importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
- }
-
- private void showLongTapDialog(ChannelInfoItem selectedItem) {
- final Context context = getContext();
- final Activity activity = getActivity();
- if (context == null || context.getResources() == null || getActivity() == null) return;
-
- final String[] commands = new String[]{
- context.getResources().getString(R.string.unsubscribe),
- context.getResources().getString(R.string.share)
- };
-
- final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
- switch (i) {
- case 0:
- deleteChannel(selectedItem);
- break;
- case 1:
- shareChannel(selectedItem);
- break;
- default:
- break;
- }
- };
-
- final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
- bannerView.setSelected(true);
-
- TextView titleView = bannerView.findViewById(R.id.itemTitleView);
- titleView.setText(selectedItem.getName());
-
- TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
- detailsView.setVisibility(View.GONE);
-
- new AlertDialog.Builder(activity)
- .setCustomTitle(bannerView)
- .setItems(commands, actions)
- .create()
- .show();
-
- }
-
- private void shareChannel(ChannelInfoItem selectedItem) {
- ShareUtils.shareUrl(getContext(), selectedItem.getName(), selectedItem.getUrl());
- }
-
- @SuppressLint("CheckResult")
- private void deleteChannel(ChannelInfoItem selectedItem) {
- subscriptionService.subscriptionTable()
- .getSubscription(selectedItem.getServiceId(), selectedItem.getUrl())
- .toObservable()
- .observeOn(Schedulers.io())
- .subscribe(getDeleteObserver());
-
- Toast.makeText(activity, getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show();
- }
-
-
-
- private Observer> getDeleteObserver() {
- return new Observer>() {
- @Override
- public void onSubscribe(Disposable d) {
- disposables.add(d);
- }
-
- @Override
- public void onNext(List subscriptionEntities) {
- subscriptionService.subscriptionTable().delete(subscriptionEntities);
- }
-
- @Override
- public void onError(Throwable exception) {
- SubscriptionFragment.this.onError(exception);
- }
-
- @Override
- public void onComplete() { }
- };
- }
-
- private void resetFragment() {
- if (disposables != null) disposables.clear();
- if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
- }
-
- ///////////////////////////////////////////////////////////////////////////
- // Subscriptions Loader
- ///////////////////////////////////////////////////////////////////////////
-
- @Override
- public void startLoading(boolean forceLoad) {
- super.startLoading(forceLoad);
- resetFragment();
-
- subscriptionService.getSubscription().toObservable()
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(getSubscriptionObserver());
- }
-
- private Observer> getSubscriptionObserver() {
- return new Observer>() {
- @Override
- public void onSubscribe(Disposable d) {
- showLoading();
- disposables.add(d);
- }
-
- @Override
- public void onNext(List subscriptions) {
- handleResult(subscriptions);
- }
-
- @Override
- public void onError(Throwable exception) {
- SubscriptionFragment.this.onError(exception);
- }
-
- @Override
- public void onComplete() {
- }
- };
- }
-
- @Override
- public void handleResult(@NonNull List result) {
- super.handleResult(result);
-
- infoListAdapter.clearStreamItemList();
-
- if (result.isEmpty()) {
- whatsNewItemListHeader.setVisibility(View.GONE);
- showEmptyState();
- } else {
- infoListAdapter.addInfoItemList(getSubscriptionItems(result));
- if (itemsListState != null) {
- itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
- itemsListState = null;
- }
- whatsNewItemListHeader.setVisibility(View.VISIBLE);
- hideLoading();
- }
- }
-
-
- private List getSubscriptionItems(List subscriptions) {
- List items = new ArrayList<>();
- for (final SubscriptionEntity subscription : subscriptions) {
- items.add(subscription.toChannelInfoItem());
- }
-
- Collections.sort(items,
- (InfoItem o1, InfoItem o2) ->
- o1.getName().compareToIgnoreCase(o2.getName()));
- return items;
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Contract
- //////////////////////////////////////////////////////////////////////////*/
-
- @Override
- public void showLoading() {
- super.showLoading();
- animateView(itemsList, false, 100);
- }
-
- @Override
- public void hideLoading() {
- super.hideLoading();
- animateView(itemsList, true, 200);
- }
-
- ///////////////////////////////////////////////////////////////////////////
- // Fragment Error Handling
- ///////////////////////////////////////////////////////////////////////////
-
- @Override
- protected boolean onError(Throwable exception) {
- resetFragment();
- if (super.onError(exception)) return true;
-
- onUnrecoverableError(exception,
- UserAction.SOMETHING_ELSE,
- "none",
- "Subscriptions",
- R.string.general_error);
- return true;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (key.equals(getString(R.string.list_view_mode_key))) {
- updateFlags |= LIST_MODE_UPDATE_FLAG;
- }
- }
-
- protected boolean isGridLayout() {
- final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
- if ("auto".equals(list_mode)) {
- final Configuration configuration = getResources().getConfiguration();
- return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
- && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
- } else {
- return "grid".equals(list_mode);
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
new file mode 100644
index 000000000..98e20a02f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
@@ -0,0 +1,421 @@
+package org.schabi.newpipe.local.subscription
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.*
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.Environment
+import android.os.Parcelable
+import android.preference.PreferenceManager
+import android.view.*
+import android.widget.Toast
+import androidx.lifecycle.ViewModelProviders
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.recyclerview.widget.GridLayoutManager
+import com.nononsenseapps.filepicker.Utils
+import com.xwray.groupie.Group
+import com.xwray.groupie.GroupAdapter
+import com.xwray.groupie.Item
+import com.xwray.groupie.Section
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import icepick.State
+import io.reactivex.disposables.CompositeDisposable
+import kotlinx.android.synthetic.main.dialog_title.view.*
+import kotlinx.android.synthetic.main.fragment_subscription.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.extractor.channel.ChannelInfoItem
+import org.schabi.newpipe.fragments.BaseStateFragment
+import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
+import org.schabi.newpipe.local.subscription.item.*
+import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
+import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
+import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH
+import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
+import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.*
+import org.schabi.newpipe.report.UserAction
+import org.schabi.newpipe.util.*
+import org.schabi.newpipe.util.AnimationUtils.animateView
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.math.floor
+import kotlin.math.max
+
+class SubscriptionFragment : BaseStateFragment() {
+ private lateinit var viewModel: SubscriptionViewModel
+ private lateinit var subscriptionManager: SubscriptionManager
+ private val disposables: CompositeDisposable = CompositeDisposable()
+
+ private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
+
+ private val groupAdapter = GroupAdapter()
+ private val feedGroupsSection = Section()
+ private var feedGroupsCarousel: FeedGroupCarouselItem? = null
+ private lateinit var importExportItem: FeedImportExportItem
+ private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
+ private val subscriptionsSection = Section()
+
+ @State @JvmField var itemsListState: Parcelable? = null
+ @State @JvmField var feedGroupsListState: Parcelable? = null
+ @State @JvmField var importExportItemExpandedState: Boolean? = null
+
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Fragment LifeCycle
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setupInitialLayout()
+ }
+
+ override fun setUserVisibleHint(isVisibleToUser: Boolean) {
+ super.setUserVisibleHint(isVisibleToUser)
+ if (activity != null && isVisibleToUser) {
+ setTitle(activity.getString(R.string.tab_subscriptions))
+ }
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ subscriptionManager = SubscriptionManager(requireContext())
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_subscription, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ setupBroadcastReceiver()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ itemsListState = items_list.layoutManager?.onSaveInstanceState()
+ feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
+ importExportItemExpandedState = importExportItem.isExpanded
+
+ if (subscriptionBroadcastReceiver != null && activity != null) {
+ LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ disposables.dispose()
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Menu
+ //////////////////////////////////////////////////////////////////////////
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ super.onCreateOptionsMenu(menu, inflater)
+
+ val supportActionBar = activity.supportActionBar
+ if (supportActionBar != null) {
+ supportActionBar.setDisplayShowTitleEnabled(true)
+ setTitle(getString(R.string.tab_subscriptions))
+ }
+ }
+
+ private fun setupBroadcastReceiver() {
+ if (activity == null) return
+
+ if (subscriptionBroadcastReceiver != null) {
+ LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
+ }
+
+ val filters = IntentFilter()
+ filters.addAction(EXPORT_COMPLETE_ACTION)
+ filters.addAction(IMPORT_COMPLETE_ACTION)
+ subscriptionBroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ items_list?.post {
+ importExportItem.isExpanded = false
+ importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
+ }
+
+ }
+ }
+
+ LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters)
+ }
+
+ private fun onImportFromServiceSelected(serviceId: Int) {
+ val fragmentManager = fm
+ NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
+ }
+
+ private fun onImportPreviousSelected() {
+ startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE)
+ }
+
+ private fun onExportSelected() {
+ val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
+ val exportName = "newpipe_subscriptions_$date.json"
+ val exportFile = File(Environment.getExternalStorageDirectory(), exportName)
+
+ startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
+ }
+
+ private fun openReorderDialog() {
+ FeedGroupReorderDialog().show(requireFragmentManager(), null)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
+ if (requestCode == REQUEST_EXPORT_CODE) {
+ val exportFile = Utils.getFileForUri(data.data!!)
+ if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
+ Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
+ } else {
+ activity.startService(Intent(activity, SubscriptionsExportService::class.java)
+ .putExtra(KEY_FILE_PATH, exportFile.absolutePath))
+ }
+ } else if (requestCode == REQUEST_IMPORT_CODE) {
+ val path = Utils.getFileForUri(data.data!!).absolutePath
+ ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java)
+ .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
+ .putExtra(KEY_VALUE, path))
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Fragment Views
+ //////////////////////////////////////////////////////////////////////////
+
+ private fun setupInitialLayout() {
+ Section().apply {
+ val carouselAdapter = GroupAdapter()
+
+ carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
+ carouselAdapter.add(feedGroupsSection)
+ carouselAdapter.add(FeedGroupAddItem())
+
+ carouselAdapter.setOnItemClickListener { item, _ ->
+ listenerFeedGroups.selected(item)
+ }
+ carouselAdapter.setOnItemLongClickListener { item, _ ->
+ if (item is FeedGroupCardItem) {
+ if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
+ return@setOnItemLongClickListener false
+ }
+ }
+ listenerFeedGroups.held(item)
+ return@setOnItemLongClickListener true
+ }
+
+ feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
+ feedGroupsSortMenuItem = HeaderWithMenuItem(
+ getString(R.string.feed_groups_header_title),
+ ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
+ menuItemOnClickListener = ::openReorderDialog
+ )
+ add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
+
+ groupAdapter.add(this)
+ }
+
+ subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
+ subscriptionsSection.setHideWhenEmpty(true)
+
+ importExportItem = FeedImportExportItem(
+ { onImportPreviousSelected() },
+ { onImportFromServiceSelected(it) },
+ { onExportSelected() },
+ importExportItemExpandedState ?: false)
+ groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
+
+ }
+
+ override fun initViews(rootView: View, savedInstanceState: Bundle?) {
+ super.initViews(rootView, savedInstanceState)
+
+ val shouldUseGridLayout = shouldUseGridLayout()
+ groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
+ items_list.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
+ spanSizeLookup = groupAdapter.spanSizeLookup
+ }
+ items_list.adapter = groupAdapter
+
+ viewModel = ViewModelProviders.of(this).get(SubscriptionViewModel::class.java)
+ viewModel.stateLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleResult) })
+ viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer { it?.let(this::handleFeedGroups) })
+ }
+
+ private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
+ val commands = arrayOf(
+ getString(R.string.share),
+ getString(R.string.unsubscribe)
+ )
+
+ val actions = DialogInterface.OnClickListener { _, i ->
+ when (i) {
+ 0 -> ShareUtils.shareUrl(requireContext(), selectedItem.name, selectedItem.url)
+ 1 -> deleteChannel(selectedItem)
+ }
+ }
+
+ val bannerView = View.inflate(requireContext(), R.layout.dialog_title, null)
+ bannerView.isSelected = true
+ bannerView.itemTitleView.text = selectedItem.name
+ bannerView.itemAdditionalDetails.visibility = View.GONE
+
+ AlertDialog.Builder(requireContext())
+ .setCustomTitle(bannerView)
+ .setItems(commands, actions)
+ .create()
+ .show()
+ }
+
+ private fun deleteChannel(selectedItem: ChannelInfoItem) {
+ disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
+ Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
+ })
+ }
+
+ override fun doInitialLoadLogic() = Unit
+ override fun startLoading(forceLoad: Boolean) = Unit
+
+ private val listenerFeedGroups = object : OnClickGesture- >() {
+ override fun selected(selectedItem: Item<*>?) {
+ when (selectedItem) {
+ is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
+ is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null)
+ }
+ }
+
+ override fun held(selectedItem: Item<*>?) {
+ when (selectedItem) {
+ is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null)
+ }
+ }
+ }
+
+ private val listenerChannelItem = object : OnClickGesture() {
+ override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm,
+ selectedItem.serviceId, selectedItem.url, selectedItem.name)
+
+ override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
+ }
+
+ override fun handleResult(result: SubscriptionState) {
+ super.handleResult(result)
+
+ val shouldUseGridLayout = shouldUseGridLayout()
+ when (result) {
+ is SubscriptionState.LoadedState -> {
+ result.subscriptions.forEach {
+ if (it is ChannelItem) {
+ it.gesturesListener = listenerChannelItem
+ it.itemVersion = when {
+ shouldUseGridLayout -> ChannelItem.ItemVersion.GRID
+ else -> ChannelItem.ItemVersion.MINI
+ }
+ }
+ }
+
+ subscriptionsSection.update(result.subscriptions)
+ subscriptionsSection.setHideWhenEmpty(false)
+
+ if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
+ items_list.post {
+ importExportItem.isExpanded = true
+ importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
+ }
+ }
+
+ if (itemsListState != null) {
+ items_list.layoutManager?.onRestoreInstanceState(itemsListState)
+ itemsListState = null
+ }
+ }
+ is SubscriptionState.ErrorState -> {
+ result.error?.let { onError(result.error) }
+ }
+ }
+ }
+
+ private fun handleFeedGroups(groups: List) {
+ feedGroupsSection.update(groups)
+
+ if (feedGroupsListState != null) {
+ feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
+ feedGroupsListState = null
+ }
+
+ if (groups.size < 2) {
+ items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_HIDE_MENU_ITEM) }
+ } else {
+ items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_SHOW_MENU_ITEM) }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Contract
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun showLoading() {
+ super.showLoading()
+ animateView(items_list, false, 100)
+ }
+
+ override fun hideLoading() {
+ super.hideLoading()
+ animateView(items_list, true, 200)
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Fragment Error Handling
+ ///////////////////////////////////////////////////////////////////////////
+
+ override fun onError(exception: Throwable): Boolean {
+ if (super.onError(exception)) return true
+
+ onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
+ return true
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Grid Mode
+ ///////////////////////////////////////////////////////////////////////////
+
+ // TODO: Move these out of this class, as it can be reused
+
+ private fun shouldUseGridLayout(): Boolean {
+ val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
+
+ return when (listMode) {
+ getString(R.string.list_view_mode_auto_key) -> {
+ val configuration = resources.configuration
+
+ (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE))
+ }
+ getString(R.string.list_view_mode_grid_key) -> true
+ else -> false
+ }
+ }
+
+ private fun getGridSpanCount(): Int {
+ val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width)
+ return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
+ }
+
+ companion object {
+ private const val REQUEST_EXPORT_CODE = 666
+ private const val REQUEST_IMPORT_CODE = 667
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt
new file mode 100644
index 000000000..92ab8cb0c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt
@@ -0,0 +1,74 @@
+package org.schabi.newpipe.local.subscription
+
+import android.content.Context
+import io.reactivex.Completable
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import org.schabi.newpipe.NewPipeDatabase
+import org.schabi.newpipe.database.subscription.SubscriptionDAO
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+import org.schabi.newpipe.extractor.ListInfo
+import org.schabi.newpipe.extractor.channel.ChannelInfo
+import org.schabi.newpipe.extractor.feed.FeedInfo
+import org.schabi.newpipe.extractor.stream.StreamInfoItem
+import org.schabi.newpipe.local.feed.FeedDatabaseManager
+
+class SubscriptionManager(context: Context) {
+ private val database = NewPipeDatabase.getInstance(context)
+ private val subscriptionTable = database.subscriptionDAO()
+ private val feedDatabaseManager = FeedDatabaseManager(context)
+
+ fun subscriptionTable(): SubscriptionDAO = subscriptionTable
+ fun subscriptions() = subscriptionTable.all
+
+ fun upsertAll(infoList: List): List {
+ val listEntities = subscriptionTable.upsertAll(
+ infoList.map { SubscriptionEntity.from(it) })
+
+ database.runInTransaction {
+ infoList.forEachIndexed { index, info ->
+ feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems)
+ }
+ }
+
+ return listEntities
+ }
+
+ fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
+ .flatMapCompletable {
+ Completable.fromRunnable {
+ it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
+ subscriptionTable.update(it)
+ feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
+ }
+ }
+
+ fun updateFromInfo(subscriptionId: Long, info: ListInfo) {
+ val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
+
+ if (info is FeedInfo) {
+ subscriptionEntity.name = info.name
+ } else if (info is ChannelInfo) {
+ subscriptionEntity.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
+ }
+
+ subscriptionTable.update(subscriptionEntity)
+ }
+
+ fun deleteSubscription(serviceId: Int, url: String): Completable {
+ return Completable.fromCallable { subscriptionTable.deleteSubscription(serviceId, url) }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ }
+
+ fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
+ database.runInTransaction {
+ val subscriptionId = subscriptionTable.insert(subscriptionEntity)
+ feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
+ }
+ }
+
+ fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
+ subscriptionTable.delete(subscriptionEntity)
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java
deleted file mode 100644
index 7d6fa5158..000000000
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionService.java
+++ /dev/null
@@ -1,162 +0,0 @@
-package org.schabi.newpipe.local.subscription;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import android.util.Log;
-
-import org.schabi.newpipe.MainActivity;
-import org.schabi.newpipe.NewPipeDatabase;
-import org.schabi.newpipe.database.AppDatabase;
-import org.schabi.newpipe.database.subscription.SubscriptionDAO;
-import org.schabi.newpipe.database.subscription.SubscriptionEntity;
-import org.schabi.newpipe.extractor.channel.ChannelInfo;
-import org.schabi.newpipe.util.ExtractorHelper;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-import io.reactivex.Completable;
-import io.reactivex.CompletableSource;
-import io.reactivex.Flowable;
-import io.reactivex.Maybe;
-import io.reactivex.Scheduler;
-import io.reactivex.functions.Function;
-import io.reactivex.schedulers.Schedulers;
-
-/**
- * Subscription Service singleton:
- * Provides a basis for channel Subscriptions.
- * Provides access to subscription table in database as well as
- * up-to-date observations on the subscribed channels
- */
-public class SubscriptionService {
-
- private static volatile SubscriptionService instance;
-
- public static SubscriptionService getInstance(@NonNull Context context) {
- SubscriptionService result = instance;
- if (result == null) {
- synchronized (SubscriptionService.class) {
- result = instance;
- if (result == null) {
- instance = (result = new SubscriptionService(context));
- }
- }
- }
-
- return result;
- }
-
- protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
- protected static final boolean DEBUG = MainActivity.DEBUG;
- private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500;
- private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4;
-
- private final AppDatabase db;
- private final Flowable
> subscription;
-
- private final Scheduler subscriptionScheduler;
-
- private SubscriptionService(Context context) {
- db = NewPipeDatabase.getInstance(context.getApplicationContext());
- subscription = getSubscriptionInfos();
-
- final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
- subscriptionScheduler = Schedulers.from(subscriptionExecutor);
- }
-
- /**
- * Part of subscription observation pipeline
- *
- * @see SubscriptionService#getSubscription()
- */
- private Flowable> getSubscriptionInfos() {
- return subscriptionTable().getAll()
- // Wait for a period of infrequent updates and return the latest update
- .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
- .share() // Share allows multiple subscribers on the same observable
- .replay(1) // Replay synchronizes subscribers to the last emitted result
- .autoConnect();
- }
-
- /**
- * Provides an observer to the latest update to the subscription table.
- *
- * This observer may be subscribed multiple times, where each subscriber obtains
- * the latest synchronized changes available, effectively share the same data
- * across all subscribers.
- *
- * This observer has a debounce cooldown, meaning if multiple updates are observed
- * in the cooldown interval, only the latest changes are emitted to the subscribers.
- * This reduces the amount of observations caused by frequent updates to the database.
- */
- @androidx.annotation.NonNull
- public Flowable> getSubscription() {
- return subscription;
- }
-
- public Maybe getChannelInfo(final SubscriptionEntity subscriptionEntity) {
- if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]");
-
- return Maybe.fromSingle(ExtractorHelper
- .getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false))
- .subscribeOn(subscriptionScheduler);
- }
-
- /**
- * Returns the database access interface for subscription table.
- */
- public SubscriptionDAO subscriptionTable() {
- return db.subscriptionDAO();
- }
-
- public Completable updateChannelInfo(final ChannelInfo info) {
- final Function, CompletableSource> update = new Function, CompletableSource>() {
- @Override
- public CompletableSource apply(@NonNull List subscriptionEntities) {
- if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]");
- if (subscriptionEntities.size() == 1) {
- SubscriptionEntity subscription = subscriptionEntities.get(0);
-
- // Subscriber count changes very often, making this check almost unnecessary.
- // Consider removing it later.
- if (!isSubscriptionUpToDate(info, subscription)) {
- subscription.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
-
- return Completable.fromRunnable(() -> subscriptionTable().update(subscription));
- }
- }
-
- return Completable.complete();
- }
- };
-
- return subscriptionTable().getSubscription(info.getServiceId(), info.getUrl())
- .firstOrError()
- .flatMapCompletable(update);
- }
-
- public List upsertAll(final List infoList) {
- final List entityList = new ArrayList<>();
- for (ChannelInfo info : infoList) entityList.add(SubscriptionEntity.from(info));
-
- return subscriptionTable().upsertAll(entityList);
- }
-
- private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
- return equalsAndNotNull(info.getUrl(), entity.getUrl()) &&
- info.getServiceId() == entity.getServiceId() &&
- info.getName().equals(entity.getName()) &&
- equalsAndNotNull(info.getAvatarUrl(), entity.getAvatarUrl()) &&
- equalsAndNotNull(info.getDescription(), entity.getDescription()) &&
- info.getSubscriberCount() == entity.getSubscriberCount();
- }
-
- private boolean equalsAndNotNull(final Object o1, final Object o2) {
- return (o1 != null && o2 != null)
- && o1.equals(o2);
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt
new file mode 100644
index 000000000..6454cc912
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt
@@ -0,0 +1,52 @@
+package org.schabi.newpipe.local.subscription
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.xwray.groupie.Group
+import io.reactivex.schedulers.Schedulers
+import org.schabi.newpipe.local.feed.FeedDatabaseManager
+import org.schabi.newpipe.local.subscription.item.ChannelItem
+import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
+import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
+import java.util.concurrent.TimeUnit
+
+class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
+ private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
+ private var subscriptionManager = SubscriptionManager(application)
+
+ private val mutableStateLiveData = MutableLiveData()
+ private val mutableFeedGroupsLiveData = MutableLiveData>()
+ val stateLiveData: LiveData = mutableStateLiveData
+ val feedGroupsLiveData: LiveData> = mutableFeedGroupsLiveData
+
+ private var feedGroupItemsDisposable = feedDatabaseManager.groups()
+ .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
+ .map { it.map(::FeedGroupCardItem) }
+ .subscribeOn(Schedulers.io())
+ .subscribe(
+ { mutableFeedGroupsLiveData.postValue(it) },
+ { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
+ )
+
+ private var stateItemsDisposable = subscriptionManager.subscriptions()
+ .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
+ .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } }
+ .subscribeOn(Schedulers.io())
+ .subscribe(
+ { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) },
+ { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) }
+ )
+
+ override fun onCleared() {
+ super.onCleared()
+ stateItemsDisposable.dispose()
+ feedGroupItemsDisposable.dispose()
+ }
+
+ sealed class SubscriptionState {
+ data class LoadedState(val subscriptions: List) : SubscriptionState()
+ data class ErrorState(val error: Throwable? = null) : SubscriptionState()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt
new file mode 100644
index 000000000..24c8d9cb8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt
@@ -0,0 +1,35 @@
+package org.schabi.newpipe.local.subscription.decoration
+
+import android.content.Context
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import org.schabi.newpipe.R
+
+class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() {
+
+ private val marginStartEnd: Int
+ private val marginTopBottom: Int
+ private val marginBetweenItems: Int
+
+ init {
+ with(context.resources) {
+ marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin)
+ marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin)
+ marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin)
+ }
+ }
+
+ override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) {
+ val childAdapterPosition = parent.getChildAdapterPosition(child)
+ val childAdapterCount = parent.adapter?.itemCount ?: 0
+
+ outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom)
+
+ if (childAdapterPosition == 0) {
+ outRect.left = marginStartEnd
+ } else if (childAdapterPosition == childAdapterCount - 1) {
+ outRect.right = marginStartEnd
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
new file mode 100644
index 000000000..27ff38a3f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
@@ -0,0 +1,354 @@
+package org.schabi.newpipe.local.subscription.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.os.Parcelable
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.Toast
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.xwray.groupie.GroupAdapter
+import com.xwray.groupie.Section
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import icepick.Icepick
+import icepick.State
+import kotlinx.android.synthetic.main.dialog_feed_group_create.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+import org.schabi.newpipe.local.subscription.FeedGroupIcon
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.*
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.ProcessingEvent
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.SuccessEvent
+import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
+import org.schabi.newpipe.local.subscription.item.PickerIconItem
+import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
+import org.schabi.newpipe.util.ThemeHelper
+import java.io.Serializable
+
+class FeedGroupDialog : DialogFragment() {
+ private lateinit var viewModel: FeedGroupDialogViewModel
+ private var groupId: Long = NO_GROUP_SELECTED
+ private var groupIcon: FeedGroupIcon? = null
+ private var groupSortOrder: Long = -1
+
+ sealed class ScreenState : Serializable {
+ object InitialScreen : ScreenState()
+ object IconPickerScreen : ScreenState()
+ object SubscriptionsPickerScreen : ScreenState()
+ object DeleteScreen : ScreenState()
+ }
+
+ @State @JvmField var selectedIcon: FeedGroupIcon? = null
+ @State @JvmField var selectedSubscriptions: HashSet = HashSet()
+ @State @JvmField var currentScreen: ScreenState = InitialScreen
+
+ @State @JvmField var subscriptionsListState: Parcelable? = null
+ @State @JvmField var iconsListState: Parcelable? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Icepick.restoreInstanceState(this, savedInstanceState)
+
+ setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
+ groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.dialog_feed_group_create, container)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return object : Dialog(requireActivity(), theme) {
+ override fun onBackPressed() {
+ if (currentScreen !is InitialScreen) {
+ showScreen(InitialScreen)
+ } else {
+ super.onBackPressed()
+ }
+ }
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+
+ iconsListState = icon_selector.layoutManager?.onSaveInstanceState()
+ subscriptionsListState = subscriptions_selector_list.layoutManager?.onSaveInstanceState()
+
+ Icepick.saveInstanceState(this, outState)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel = ViewModelProviders.of(this, FeedGroupDialogViewModel.Factory(requireContext(), groupId))
+ .get(FeedGroupDialogViewModel::class.java)
+
+ viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
+ viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { setupSubscriptionPicker(it.first, it.second) })
+ viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
+ when (it) {
+ ProcessingEvent -> disableInput()
+ SuccessEvent -> dismiss()
+ }
+ })
+
+ setupIconPicker()
+ setupListeners()
+
+ showScreen(currentScreen)
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Setup
+ ///////////////////////////////////////////////////////////////////////////
+
+ private fun setupListeners() {
+ delete_button.setOnClickListener { showScreen(DeleteScreen) }
+
+ cancel_button.setOnClickListener {
+ when (currentScreen) {
+ InitialScreen -> dismiss()
+ else -> showScreen(InitialScreen)
+ }
+ }
+
+ group_name_input_container.error = null
+ group_name_input.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {}
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ if (group_name_input_container.isErrorEnabled && !s.isNullOrBlank()) {
+ group_name_input_container.error = null
+ }
+ }
+ })
+
+ confirm_button.setOnClickListener {
+ when (currentScreen) {
+ InitialScreen -> handlePositiveButtonInitialScreen()
+ DeleteScreen -> viewModel.deleteGroup()
+ else -> showScreen(InitialScreen)
+ }
+ }
+ }
+
+ private fun handlePositiveButtonInitialScreen() {
+ val name = group_name_input.text.toString().trim()
+ val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL
+
+ if (name.isBlank()) {
+ group_name_input_container.error = getString(R.string.feed_group_dialog_empty_name)
+ group_name_input.text = null
+ group_name_input.requestFocus()
+ return
+ } else {
+ group_name_input_container.error = null
+ }
+
+ if (selectedSubscriptions.isEmpty()) {
+ Toast.makeText(requireContext(), getString(R.string.feed_group_dialog_empty_selection), Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ when (groupId) {
+ NO_GROUP_SELECTED -> viewModel.createGroup(name, icon, selectedSubscriptions)
+ else -> viewModel.updateGroup(name, icon, selectedSubscriptions, groupSortOrder)
+ }
+ }
+
+ private fun handleGroup(feedGroupEntity: FeedGroupEntity? = null) {
+ val icon = feedGroupEntity?.icon ?: FeedGroupIcon.ALL
+ val name = feedGroupEntity?.name ?: ""
+ groupIcon = feedGroupEntity?.icon
+ groupSortOrder = feedGroupEntity?.sortOrder ?: -1
+
+ icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext()))
+
+ if (group_name_input.text.isNullOrBlank()) {
+ group_name_input.setText(name)
+ }
+ }
+
+ private fun setupSubscriptionPicker(subscriptions: List, selectedSubscriptions: Set) {
+ this.selectedSubscriptions.addAll(selectedSubscriptions)
+ val useGridLayout = subscriptions.isNotEmpty()
+
+ val groupAdapter = GroupAdapter()
+ groupAdapter.spanCount = if (useGridLayout) 4 else 1
+
+ val selectedCountText = getString(R.string.feed_group_dialog_selection_count, this.selectedSubscriptions.size)
+ selected_subscription_count_view.text = selectedCountText
+ subscriptions_selector_header_info.text = selectedCountText
+
+ Section().apply {
+ addAll(subscriptions.map {
+ val isSelected = this@FeedGroupDialog.selectedSubscriptions.contains(it.uid)
+ PickerSubscriptionItem(it, isSelected)
+ })
+ setPlaceholder(EmptyPlaceholderItem())
+
+ groupAdapter.add(this)
+ }
+
+ subscriptions_selector_list.apply {
+ layoutManager = if (useGridLayout) {
+ GridLayoutManager(requireContext(), groupAdapter.spanCount, RecyclerView.VERTICAL, false)
+ } else {
+ LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
+ }
+
+ adapter = groupAdapter
+
+ if (subscriptionsListState != null) {
+ layoutManager?.onRestoreInstanceState(subscriptionsListState)
+ subscriptionsListState = null
+ }
+ }
+
+ groupAdapter.setOnItemClickListener { item, _ ->
+ when (item) {
+ is PickerSubscriptionItem -> {
+ val subscriptionId = item.subscriptionEntity.uid
+
+ val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) {
+ this.selectedSubscriptions.remove(subscriptionId)
+ false
+ } else {
+ this.selectedSubscriptions.add(subscriptionId)
+ true
+ }
+
+ item.isSelected = isSelected
+ item.notifyChanged(PickerSubscriptionItem.UPDATE_SELECTED)
+
+ val updateSelectedCountText = getString(R.string.feed_group_dialog_selection_count, this.selectedSubscriptions.size)
+ selected_subscription_count_view.text = updateSelectedCountText
+ subscriptions_selector_header_info.text = updateSelectedCountText
+ }
+ }
+ }
+
+ select_channel_button.setOnClickListener {
+ subscriptions_selector_list.scrollToPosition(0)
+ showScreen(SubscriptionsPickerScreen)
+ }
+ }
+
+ private fun setupIconPicker() {
+ val groupAdapter = GroupAdapter()
+ groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
+
+ icon_selector.apply {
+ layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
+ adapter = groupAdapter
+
+ if (iconsListState != null) {
+ layoutManager?.onRestoreInstanceState(iconsListState)
+ iconsListState = null
+ }
+ }
+
+ groupAdapter.setOnItemClickListener { item, _ ->
+ when (item) {
+ is PickerIconItem -> {
+ selectedIcon = item.icon
+ icon_preview.setImageResource(item.iconRes)
+
+ showScreen(InitialScreen)
+ }
+ }
+ }
+ icon_preview.setOnClickListener {
+ icon_selector.scrollToPosition(0)
+ showScreen(IconPickerScreen)
+ }
+
+ if (groupId == NO_GROUP_SELECTED) {
+ val icon = selectedIcon ?: FeedGroupIcon.ALL
+ icon_preview.setImageResource(icon.getDrawableRes(requireContext()))
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Screen Selector
+ ///////////////////////////////////////////////////////////////////////////
+
+ private fun showScreen(screen: ScreenState) {
+ currentScreen = screen
+
+ options_root.onlyVisibleIn(InitialScreen)
+ icon_selector.onlyVisibleIn(IconPickerScreen)
+ subscriptions_selector.onlyVisibleIn(SubscriptionsPickerScreen)
+ delete_screen_message.onlyVisibleIn(DeleteScreen)
+
+ separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
+ cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen)
+
+ confirm_button.setText(when {
+ currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
+ else -> android.R.string.ok
+ })
+
+ delete_button.visibility = when {
+ currentScreen != InitialScreen -> View.GONE
+ groupId == NO_GROUP_SELECTED -> View.GONE
+ else -> View.VISIBLE
+ }
+
+ if (currentScreen != InitialScreen) hideKeyboard()
+ }
+
+ private fun View.onlyVisibleIn(vararg screens: ScreenState) {
+ visibility = when (currentScreen) {
+ in screens -> View.VISIBLE
+ else -> View.GONE
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Utils
+ ///////////////////////////////////////////////////////////////////////////
+
+ private fun hideKeyboard() {
+ val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
+ group_name_input.clearFocus()
+ }
+
+ private fun disableInput() {
+ delete_button?.isEnabled = false
+ confirm_button?.isEnabled = false
+ cancel_button?.isEnabled = false
+ isCancelable = false
+
+ hideKeyboard()
+ }
+
+ companion object {
+ private const val KEY_GROUP_ID = "KEY_GROUP_ID"
+ private const val NO_GROUP_SELECTED = -1L
+
+ fun newInstance(groupId: Long = NO_GROUP_SELECTED): FeedGroupDialog {
+ val dialog = FeedGroupDialog()
+
+ dialog.arguments = Bundle().apply {
+ putLong(KEY_GROUP_ID, groupId)
+ }
+
+ return dialog
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
new file mode 100644
index 000000000..bd57a2639
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
@@ -0,0 +1,87 @@
+package org.schabi.newpipe.local.subscription.dialog
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.disposables.Disposable
+import io.reactivex.functions.BiFunction
+import io.reactivex.schedulers.Schedulers
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+import org.schabi.newpipe.local.feed.FeedDatabaseManager
+import org.schabi.newpipe.local.subscription.FeedGroupIcon
+import org.schabi.newpipe.local.subscription.SubscriptionManager
+
+
+class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
+ class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return FeedGroupDialogViewModel(context.applicationContext, groupId) as T
+ }
+ }
+
+ private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
+ private var subscriptionManager = SubscriptionManager(applicationContext)
+
+ private val mutableGroupLiveData = MutableLiveData()
+ private val mutableSubscriptionsLiveData = MutableLiveData, Set>>()
+ private val mutableDialogEventLiveData = MutableLiveData()
+ val groupLiveData: LiveData = mutableGroupLiveData
+ val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData
+ val dialogEventLiveData: LiveData = mutableDialogEventLiveData
+
+ private var actionProcessingDisposable: Disposable? = null
+
+ private var feedGroupDisposable = feedDatabaseManager.getGroup(groupId)
+ .subscribeOn(Schedulers.io())
+ .subscribe(mutableGroupLiveData::postValue)
+
+ private var subscriptionsDisposable = Flowable
+ .combineLatest(subscriptionManager.subscriptions(), feedDatabaseManager.subscriptionIdsForGroup(groupId),
+ BiFunction { t1: List, t2: List -> t1 to t2.toSet() })
+ .subscribeOn(Schedulers.io())
+ .subscribe(mutableSubscriptionsLiveData::postValue)
+
+ override fun onCleared() {
+ super.onCleared()
+ actionProcessingDisposable?.dispose()
+ subscriptionsDisposable.dispose()
+ feedGroupDisposable.dispose()
+ }
+
+ fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set) {
+ doAction(feedDatabaseManager.createGroup(name, selectedIcon)
+ .flatMapCompletable {
+ feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
+ })
+ }
+
+ fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set, sortOrder: Long) {
+ doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
+ .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))))
+ }
+
+ fun deleteGroup() {
+ doAction(feedDatabaseManager.deleteGroup(groupId))
+ }
+
+ private fun doAction(completable: Completable) {
+ if (actionProcessingDisposable == null) {
+ mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent
+
+ actionProcessingDisposable = completable
+ .subscribeOn(Schedulers.io())
+ .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
+ }
+ }
+
+ sealed class DialogEvent {
+ object ProcessingEvent : DialogEvent()
+ object SuccessEvent : DialogEvent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt
new file mode 100644
index 000000000..17ee89c87
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt
@@ -0,0 +1,109 @@
+package org.schabi.newpipe.local.subscription.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.xwray.groupie.GroupAdapter
+import com.xwray.groupie.TouchCallback
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import icepick.Icepick
+import icepick.State
+import kotlinx.android.synthetic.main.dialog_feed_group_reorder.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.*
+import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
+import org.schabi.newpipe.util.ThemeHelper
+import java.util.*
+import kotlin.collections.ArrayList
+
+class FeedGroupReorderDialog : DialogFragment() {
+ private lateinit var viewModel: FeedGroupReorderDialogViewModel
+
+ @State @JvmField var groupOrderedIdList = ArrayList()
+ private val groupAdapter = GroupAdapter()
+ private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Icepick.restoreInstanceState(this, savedInstanceState)
+
+ setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.dialog_feed_group_reorder, container)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java)
+ viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
+ viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
+ when (it) {
+ ProcessingEvent -> disableInput()
+ SuccessEvent -> dismiss()
+ }
+ })
+
+ feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
+ feed_groups_list.adapter = groupAdapter
+ itemTouchHelper.attachToRecyclerView(feed_groups_list)
+
+ confirm_button.setOnClickListener {
+ viewModel.updateOrder(groupOrderedIdList)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ Icepick.saveInstanceState(this, outState)
+ }
+
+ private fun handleGroups(list: List) {
+ val groupList: List
+
+ if (groupOrderedIdList.isEmpty()) {
+ groupList = list
+ groupOrderedIdList.addAll(groupList.map { it.uid })
+ } else {
+ groupList = list.sortedBy { groupOrderedIdList.indexOf(it.uid) }
+ }
+
+ groupAdapter.update(groupList.map { FeedGroupReorderItem(it, itemTouchHelper) })
+ }
+
+ private fun disableInput() {
+ confirm_button?.isEnabled = false
+ isCancelable = false
+ }
+
+ private fun getItemTouchCallback(): SimpleCallback {
+ return object : TouchCallback() {
+
+ override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder): Boolean {
+ val sourceIndex = source.adapterPosition
+ val targetIndex = target.adapterPosition
+
+ groupAdapter.notifyItemMoved(sourceIndex, targetIndex)
+ Collections.swap(groupOrderedIdList, sourceIndex, targetIndex)
+
+ return true
+ }
+
+ override fun isLongPressDragEnabled(): Boolean = false
+ override fun isItemViewSwipeEnabled(): Boolean = false
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt
new file mode 100644
index 000000000..8ef5bb55c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt
@@ -0,0 +1,52 @@
+package org.schabi.newpipe.local.subscription.dialog
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import io.reactivex.Completable
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.local.feed.FeedDatabaseManager
+
+class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewModel(application) {
+ private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
+
+ private val mutableGroupsLiveData = MutableLiveData>()
+ private val mutableDialogEventLiveData = MutableLiveData()
+ val groupsLiveData: LiveData> = mutableGroupsLiveData
+ val dialogEventLiveData: LiveData = mutableDialogEventLiveData
+
+ private var actionProcessingDisposable: Disposable? = null
+
+ private var groupsDisposable = feedDatabaseManager.groups()
+ .limit(1)
+ .subscribeOn(Schedulers.io())
+ .subscribe(mutableGroupsLiveData::postValue)
+
+ override fun onCleared() {
+ super.onCleared()
+ actionProcessingDisposable?.dispose()
+ groupsDisposable.dispose()
+ }
+
+ fun updateOrder(groupIdList: List) {
+ doAction(feedDatabaseManager.updateGroupsOrder(groupIdList))
+ }
+
+ private fun doAction(completable: Completable) {
+ if (actionProcessingDisposable == null) {
+ mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent
+
+ actionProcessingDisposable = completable
+ .subscribeOn(Schedulers.io())
+ .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
+ }
+ }
+
+ sealed class DialogEvent {
+ object ProcessingEvent : DialogEvent()
+ object SuccessEvent : DialogEvent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt
new file mode 100644
index 000000000..928f93a47
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt
@@ -0,0 +1,65 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.content.Context
+import com.nostra13.universalimageloader.core.ImageLoader
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.list_channel_item.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.channel.ChannelInfoItem
+import org.schabi.newpipe.util.ImageDisplayConstants
+import org.schabi.newpipe.util.Localization
+import org.schabi.newpipe.util.OnClickGesture
+
+
+class ChannelItem(
+ private val infoItem: ChannelInfoItem,
+ private val subscriptionId: Long = -1L,
+ var itemVersion: ItemVersion = ItemVersion.NORMAL,
+ var gesturesListener: OnClickGesture? = null
+) : Item() {
+
+ override fun getId(): Long = if (subscriptionId == -1L) super.getId() else subscriptionId
+
+ enum class ItemVersion { NORMAL, MINI, GRID }
+
+ override fun getLayout(): Int = when (itemVersion) {
+ ItemVersion.NORMAL -> R.layout.list_channel_item
+ ItemVersion.MINI -> R.layout.list_channel_mini_item
+ ItemVersion.GRID -> R.layout.list_channel_grid_item
+ }
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.itemTitleView.text = infoItem.name
+ viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context)
+ if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description
+
+ ImageLoader.getInstance().displayImage(infoItem.thumbnailUrl, viewHolder.itemThumbnailView,
+ ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS)
+
+ gesturesListener?.run {
+ viewHolder.containerView.setOnClickListener { selected(infoItem) }
+ viewHolder.containerView.setOnLongClickListener { held(infoItem); true }
+ }
+ }
+
+ private fun getDetailLine(context: Context): String {
+ var details = if (infoItem.subscriberCount >= 0) {
+ Localization.shortSubscriberCount(context, infoItem.subscriberCount)
+ } else {
+ context.getString(R.string.subscribers_count_not_available)
+ }
+
+ if (itemVersion == ItemVersion.NORMAL) {
+ if (infoItem.streamCount >= 0) {
+ val formattedVideoAmount = Localization.localizeStreamCount(context, infoItem.streamCount)
+ details = Localization.concatenateStrings(details, formattedVideoAmount)
+ }
+ }
+ return details
+ }
+
+ override fun getSpanSize(spanCount: Int, position: Int): Int {
+ return if (itemVersion == ItemVersion.GRID) 1 else spanCount
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt
new file mode 100644
index 000000000..0c651dc69
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt
@@ -0,0 +1,10 @@
+package org.schabi.newpipe.local.subscription.item
+
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import org.schabi.newpipe.R
+
+class EmptyPlaceholderItem : Item() {
+ override fun getLayout(): Int = R.layout.list_empty_view
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt
new file mode 100644
index 000000000..309f82bbc
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt
@@ -0,0 +1,10 @@
+package org.schabi.newpipe.local.subscription.item
+
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import org.schabi.newpipe.R
+
+class FeedGroupAddItem : Item() {
+ override fun getLayout(): Int = R.layout.feed_group_add_new_item
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt
new file mode 100644
index 000000000..a757dc5b3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardItem.kt
@@ -0,0 +1,30 @@
+package org.schabi.newpipe.local.subscription.item
+
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.feed_group_card_item.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.local.subscription.FeedGroupIcon
+
+data class FeedGroupCardItem(
+ val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
+ val name: String,
+ val icon: FeedGroupIcon
+) : Item() {
+ constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)
+
+ override fun getId(): Long {
+ return when (groupId) {
+ FeedGroupEntity.GROUP_ALL_ID -> super.getId()
+ else -> groupId
+ }
+ }
+
+ override fun getLayout(): Int = R.layout.feed_group_card_item
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.title.text = name
+ viewHolder.icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt
new file mode 100644
index 000000000..bde3c604a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt
@@ -0,0 +1,57 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.content.Context
+import android.os.Parcelable
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.xwray.groupie.GroupAdapter
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.feed_item_carousel.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration
+
+class FeedGroupCarouselItem(context: Context, private val carouselAdapter: GroupAdapter) : Item() {
+ private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context)
+
+ private var linearLayoutManager: LinearLayoutManager? = null
+ private var listState: Parcelable? = null
+
+ override fun getLayout() = R.layout.feed_item_carousel
+
+ fun onSaveInstanceState(): Parcelable? {
+ listState = linearLayoutManager?.onSaveInstanceState()
+ return listState
+ }
+
+ fun onRestoreInstanceState(state: Parcelable?) {
+ linearLayoutManager?.onRestoreInstanceState(state)
+ listState = state
+ }
+
+ override fun createViewHolder(itemView: View): GroupieViewHolder {
+ val viewHolder = super.createViewHolder(itemView)
+
+ linearLayoutManager = LinearLayoutManager(itemView.context, RecyclerView.HORIZONTAL, false)
+
+ viewHolder.recycler_view.apply {
+ layoutManager = linearLayoutManager
+ adapter = carouselAdapter
+ addItemDecoration(feedGroupCarouselDecoration)
+ }
+
+ return viewHolder
+ }
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.recycler_view.apply { adapter = carouselAdapter }
+ linearLayoutManager?.onRestoreInstanceState(listState)
+ }
+
+ override fun unbind(viewHolder: GroupieViewHolder) {
+ super.unbind(viewHolder)
+
+ listState = linearLayoutManager?.onSaveInstanceState()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt
new file mode 100644
index 000000000..cf010af7f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt
@@ -0,0 +1,48 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.view.MotionEvent
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.ItemTouchHelper.DOWN
+import androidx.recyclerview.widget.ItemTouchHelper.UP
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import com.xwray.groupie.kotlinandroidextensions.Item
+import kotlinx.android.synthetic.main.feed_group_reorder_item.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity
+import org.schabi.newpipe.local.subscription.FeedGroupIcon
+
+data class FeedGroupReorderItem(
+ val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
+ val name: String,
+ val icon: FeedGroupIcon,
+ val dragCallback: ItemTouchHelper
+) : Item() {
+ constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper)
+ : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
+
+ override fun getId(): Long {
+ return when (groupId) {
+ FeedGroupEntity.GROUP_ALL_ID -> super.getId()
+ else -> groupId
+ }
+ }
+
+ override fun getLayout(): Int = R.layout.feed_group_reorder_item
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.group_name.text = name
+ viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
+ viewHolder.handle.setOnTouchListener { _, event ->
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+ dragCallback.startDrag(viewHolder)
+ return@setOnTouchListener true
+ }
+
+ false
+ }
+ }
+
+ override fun getDragDirs(): Int {
+ return UP or DOWN
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt
new file mode 100644
index 000000000..ab47564ce
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt
@@ -0,0 +1,116 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import com.xwray.groupie.kotlinandroidextensions.Item
+import kotlinx.android.synthetic.main.feed_import_export_group.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.extractor.NewPipe
+import org.schabi.newpipe.extractor.exceptions.ExtractionException
+import org.schabi.newpipe.util.AnimationUtils
+import org.schabi.newpipe.util.ServiceHelper
+import org.schabi.newpipe.util.ThemeHelper
+import org.schabi.newpipe.views.CollapsibleView
+
+class FeedImportExportItem(
+ val onImportPreviousSelected: () -> Unit,
+ val onImportFromServiceSelected: (Int) -> Unit,
+ val onExportSelected: () -> Unit,
+ var isExpanded: Boolean = false
+) : Item() {
+ companion object {
+ const val REFRESH_EXPANDED_STATUS = 123
+ }
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) {
+ if (payloads.contains(REFRESH_EXPANDED_STATUS)) {
+ viewHolder.import_export_options.apply { if (isExpanded) expand() else collapse() }
+ return
+ }
+
+ super.bind(viewHolder, position, payloads)
+ }
+
+ override fun getLayout(): Int = R.layout.feed_import_export_group
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ if (viewHolder.import_from_options.childCount == 0) setupImportFromItems(viewHolder.import_from_options)
+ if (viewHolder.export_to_options.childCount == 0) setupExportToItems(viewHolder.export_to_options)
+
+ expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
+ expandIconListener = CollapsibleView.StateListener { newState ->
+ AnimationUtils.animateRotation(viewHolder.import_export_expand_icon,
+ 250, if (newState == CollapsibleView.COLLAPSED) 0 else 180)
+ }
+
+ viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED
+ viewHolder.import_export_expand_icon.rotation = if (isExpanded) 180F else 0F
+ viewHolder.import_export_options.ready()
+
+ viewHolder.import_export_options.addListener(expandIconListener)
+ viewHolder.import_export.setOnClickListener {
+ viewHolder.import_export_options.switchState()
+ isExpanded = viewHolder.import_export_options.currentState == CollapsibleView.EXPANDED
+ }
+ }
+
+ override fun unbind(viewHolder: GroupieViewHolder) {
+ super.unbind(viewHolder)
+ expandIconListener?.let { viewHolder.import_export_options.removeListener(it) }
+ expandIconListener = null
+ }
+
+ private var expandIconListener: CollapsibleView.StateListener? = null
+
+ private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View {
+ val itemRoot = View.inflate(container.context, R.layout.subscription_import_export_item, null)
+ val titleView = itemRoot.findViewById(android.R.id.text1)
+ val iconView = itemRoot.findViewById(android.R.id.icon1)
+
+ titleView.text = title
+ iconView.setImageResource(icon)
+
+ container.addView(itemRoot)
+ return itemRoot
+ }
+
+ private fun setupImportFromItems(listHolder: ViewGroup) {
+ val previousBackupItem = addItemView(listHolder.context.getString(R.string.previous_export),
+ ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder)
+ previousBackupItem.setOnClickListener { onImportPreviousSelected() }
+
+ val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE
+ val services = listHolder.context.resources.getStringArray(R.array.service_list)
+ for (serviceName in services) {
+ try {
+ val service = NewPipe.getService(serviceName)
+
+ val subscriptionExtractor = service.subscriptionExtractor ?: continue
+
+ val supportedSources = subscriptionExtractor.supportedSources
+ if (supportedSources.isEmpty()) continue
+
+ val itemView = addItemView(serviceName, ServiceHelper.getIcon(service.serviceId), listHolder)
+ val iconView = itemView.findViewById(android.R.id.icon1)
+ iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN)
+
+ itemView.setOnClickListener { onImportFromServiceSelected(service.serviceId) }
+ } catch (e: ExtractionException) {
+ throw RuntimeException("Services array contains an entry that it's not a valid service name ($serviceName)", e)
+ }
+
+ }
+ }
+
+ private fun setupExportToItems(listHolder: ViewGroup) {
+ val previousBackupItem = addItemView(listHolder.context.getString(R.string.file),
+ ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder)
+ previousBackupItem.setOnClickListener { onExportSelected() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt
new file mode 100644
index 000000000..367605f46
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt
@@ -0,0 +1,19 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.view.View.OnClickListener
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.header_item.*
+import org.schabi.newpipe.R
+
+class HeaderItem(val title: String, private val onClickListener: (() -> Unit)? = null) : Item() {
+
+ override fun getLayout(): Int = R.layout.header_item
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.header_title.text = title
+
+ val listener: OnClickListener? = if (onClickListener != null) OnClickListener { onClickListener.invoke() } else null
+ viewHolder.root.setOnClickListener(listener)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt
new file mode 100644
index 000000000..5ffdfe7c1
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt
@@ -0,0 +1,48 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.view.View.*
+import androidx.annotation.DrawableRes
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import com.xwray.groupie.kotlinandroidextensions.Item
+import kotlinx.android.synthetic.main.header_with_menu_item.*
+import org.schabi.newpipe.R
+
+class HeaderWithMenuItem(
+ val title: String,
+ @DrawableRes val itemIcon: Int = 0,
+ private val onClickListener: (() -> Unit)? = null,
+ private val menuItemOnClickListener: (() -> Unit)? = null
+) : Item() {
+ companion object {
+ const val PAYLOAD_SHOW_MENU_ITEM = 1
+ const val PAYLOAD_HIDE_MENU_ITEM = 2
+ }
+
+ override fun getLayout(): Int = R.layout.header_with_menu_item
+
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) {
+ if (payloads.contains(PAYLOAD_SHOW_MENU_ITEM)) {
+ viewHolder.header_menu_item.visibility = VISIBLE
+ return
+ } else if (payloads.contains(PAYLOAD_HIDE_MENU_ITEM)) {
+ viewHolder.header_menu_item.visibility = GONE
+ return
+ }
+
+ super.bind(viewHolder, position, payloads)
+ }
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.header_title.text = title
+ viewHolder.header_menu_item.setImageResource(itemIcon)
+
+ val listener: OnClickListener? =
+ onClickListener?.let { OnClickListener { onClickListener.invoke() } }
+ viewHolder.root.setOnClickListener(listener)
+
+ val menuItemListener: OnClickListener? =
+ menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
+ viewHolder.header_menu_item.setOnClickListener(menuItemListener)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt
new file mode 100644
index 000000000..fedec9880
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerIconItem.kt
@@ -0,0 +1,19 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.content.Context
+import androidx.annotation.DrawableRes
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.picker_icon_item.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.local.subscription.FeedGroupIcon
+
+class PickerIconItem(context: Context, val icon: FeedGroupIcon) : Item() {
+ @DrawableRes val iconRes: Int = icon.getDrawableRes(context)
+
+ override fun getLayout(): Int = R.layout.picker_icon_item
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ viewHolder.icon_view.setImageResource(iconRes)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt
new file mode 100644
index 000000000..21c74b09f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt
@@ -0,0 +1,51 @@
+package org.schabi.newpipe.local.subscription.item
+
+import android.view.View
+import com.nostra13.universalimageloader.core.DisplayImageOptions
+import com.nostra13.universalimageloader.core.ImageLoader
+import com.xwray.groupie.kotlinandroidextensions.Item
+import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
+import kotlinx.android.synthetic.main.picker_subscription_item.*
+import org.schabi.newpipe.R
+import org.schabi.newpipe.database.subscription.SubscriptionEntity
+import org.schabi.newpipe.util.AnimationUtils
+import org.schabi.newpipe.util.AnimationUtils.animateView
+import org.schabi.newpipe.util.ImageDisplayConstants
+
+data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, var isSelected: Boolean = false) : Item() {
+ companion object {
+ const val UPDATE_SELECTED = 123
+
+ val IMAGE_LOADING_OPTIONS: DisplayImageOptions = ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
+ }
+
+ override fun getLayout(): Int = R.layout.picker_subscription_item
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList) {
+ if (payloads.contains(UPDATE_SELECTED)) {
+ animateView(viewHolder.selected_highlight, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150)
+ return
+ }
+
+ super.bind(viewHolder, position, payloads)
+ }
+
+ override fun bind(viewHolder: GroupieViewHolder, position: Int) {
+ ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, viewHolder.thumbnail_view, IMAGE_LOADING_OPTIONS)
+
+ viewHolder.title_view.text = subscriptionEntity.name
+ viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE
+ }
+
+ override fun unbind(viewHolder: GroupieViewHolder) {
+ super.unbind(viewHolder)
+
+ viewHolder.selected_highlight.animate().setListener(null).cancel()
+ viewHolder.selected_highlight.visibility = View.GONE
+ viewHolder.selected_highlight.alpha = 1F
+ }
+
+ override fun getId(): Long {
+ return subscriptionEntity.uid
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java
index 6b607cdca..e970ebfa4 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/BaseImportExportService.java
@@ -34,10 +34,9 @@ import android.widget.Toast;
import org.reactivestreams.Publisher;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
+import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
-import org.schabi.newpipe.local.subscription.ImportExportEventListener;
-import org.schabi.newpipe.local.subscription.SubscriptionService;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -57,7 +56,7 @@ public abstract class BaseImportExportService extends Service {
protected NotificationManagerCompat notificationManager;
protected NotificationCompat.Builder notificationBuilder;
- protected SubscriptionService subscriptionService;
+ protected SubscriptionManager subscriptionManager;
protected final CompositeDisposable disposables = new CompositeDisposable();
protected final PublishProcessor notificationUpdater = PublishProcessor.create();
@@ -70,7 +69,7 @@ public abstract class BaseImportExportService extends Service {
@Override
public void onCreate() {
super.onCreate();
- subscriptionService = SubscriptionService.getInstance(this);
+ subscriptionManager = new SubscriptionManager(this);
setupNotification();
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java
similarity index 87%
rename from app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java
rename to app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java
index 01c0427f3..788073ee5 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportEventListener.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.local.subscription;
+package org.schabi.newpipe.local.subscription.services;
public interface ImportExportEventListener {
/**
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
similarity index 98%
rename from app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java
rename to app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
index ebfff9fe2..5b5ebf702 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/ImportExportJsonHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/ImportExportJsonHelper.java
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package org.schabi.newpipe.local.subscription;
+package org.schabi.newpipe.local.subscription.services;
import androidx.annotation.Nullable;
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
index 31cd4b603..358024574 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
@@ -29,7 +29,6 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
-import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
import java.io.File;
import java.io.FileNotFoundException;
@@ -96,7 +95,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
private void startExport() {
showToast(R.string.export_ongoing);
- subscriptionService.subscriptionTable()
+ subscriptionManager.subscriptionTable()
.getAll()
.take(1)
.map(subscriptionEntities -> {
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
index 62c1dfeb9..0d2f3757f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java
@@ -33,7 +33,6 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
-import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -180,6 +179,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
.observeOn(Schedulers.io())
.doOnNext(getNotificationsConsumer())
+
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.map(upsertBatch())
@@ -204,6 +204,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
@Override
public void onError(Throwable error) {
+ Log.e(TAG, "Got an error!", error);
handleError(error);
}
@@ -242,7 +243,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
if (n.isOnNext()) infoList.add(n.getValue());
}
- return subscriptionService.upsertAll(infoList);
+ return subscriptionManager.upsertAll(infoList);
};
}
diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java
index 2cca9305a..f4f3e31b6 100644
--- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java
+++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java
@@ -16,6 +16,7 @@ public enum UserAction {
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk"),
REQUESTED_COMMENTS("requested comments"),
+ REQUESTED_FEED("requested feed"),
DELETE_FROM_HISTORY("delete from history"),
PLAY_STREAM("Play stream"),
DOWNLOAD_POSTPROCESSING("download post-processing"),
diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
index e0003ccaa..6c765dc3d 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java
@@ -23,7 +23,7 @@ package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
-import android.preference.PreferenceManager;
+import androidx.preference.PreferenceManager;
import androidx.annotation.NonNull;
import org.schabi.newpipe.R;
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
index 7064aec33..9ee12facc 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java
@@ -18,9 +18,9 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
+import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
-import org.schabi.newpipe.local.subscription.SubscriptionService;
import java.util.List;
import java.util.Vector;
@@ -99,8 +99,8 @@ public class SelectChannelFragment extends DialogFragment {
emptyView.setVisibility(View.GONE);
- SubscriptionService subscriptionService = SubscriptionService.getInstance(getContext());
- subscriptionService.getSubscription().toObservable()
+ SubscriptionManager subscriptionManager = new SubscriptionManager(getContext());
+ subscriptionManager.subscriptions().toObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionObserver());
diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt
new file mode 100644
index 000000000..4bc59fcee
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/custom/DurationListPreference.kt
@@ -0,0 +1,46 @@
+package org.schabi.newpipe.settings.custom
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.ListPreference
+import org.schabi.newpipe.util.Localization
+
+/**
+ * An extension of a common ListPreference where it sets the duration values to human readable strings.
+ *
+ * The values in the entry values array will be interpreted as seconds. If the value of a specific position
+ * is less than or equals to zero, its original entry title will be used.
+ *
+ * If the entry values array have anything other than numbers in it, an exception will be raised.
+ */
+class DurationListPreference : ListPreference {
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context?) : super(context)
+
+ override fun onAttached() {
+ super.onAttached()
+
+ val originalEntryTitles = entries
+ val originalEntryValues = entryValues
+ val newEntryTitles = arrayOfNulls(originalEntryValues.size)
+
+ for (i in originalEntryValues.indices) {
+ val currentDurationValue: Int
+ try {
+ currentDurationValue = (originalEntryValues[i] as String).toInt()
+ } catch (e: NumberFormatException) {
+ throw RuntimeException("Invalid number was set in the preference entry values array", e)
+ }
+
+ if (currentDurationValue <= 0) {
+ newEntryTitles[i] = originalEntryTitles[i]
+ } else {
+ newEntryTitles[i] = Localization.localizeDuration(context, currentDurationValue)
+ }
+ }
+
+ entries = newEntryTitles
+ }
+}
\ No newline at end of file
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 cba3c4534..cc40298b9 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
@@ -218,7 +218,7 @@ public abstract class Tab {
@Override
public String getTabName(Context context) {
- return context.getString(R.string.fragment_whats_new);
+ return context.getString(R.string.fragment_feed_title);
}
@DrawableRes
diff --git a/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt b/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt
new file mode 100644
index 000000000..8d24cb04e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/ConstantsKt.kt
@@ -0,0 +1,6 @@
+package org.schabi.newpipe.util
+
+/**
+ * Default duration when using throttle functions across the app, in milliseconds.
+ */
+const val DEFAULT_THROTTLE_TIMEOUT = 120L
diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
index 0cebe5af3..cf4477223 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java
@@ -31,18 +31,23 @@ import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
+import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
+import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
+import org.schabi.newpipe.extractor.feed.FeedExtractor;
+import org.schabi.newpipe.extractor.feed.FeedInfo;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchInfo;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamInfoItem;
+import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
@@ -131,6 +136,22 @@ public final class ExtractorHelper {
ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl));
}
+ public static Single> getFeedInfoFallbackToChannelInfo(final int serviceId,
+ final String url) {
+ final Maybe> maybeFeedInfo = Maybe.fromCallable(() -> {
+ final StreamingService service = NewPipe.getService(serviceId);
+ final FeedExtractor feedExtractor = service.getFeedExtractor(url);
+
+ if (feedExtractor == null) {
+ return null;
+ }
+
+ return FeedInfo.getInfo(feedExtractor);
+ });
+
+ return maybeFeedInfo.switchIfEmpty(getChannelInfo(serviceId, url, true));
+ }
+
public static Single getCommentsInfo(final int serviceId,
final String url,
boolean forceLoad) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java
index 47b914bde..9c8fc25b8 100644
--- a/app/src/main/java/org/schabi/newpipe/util/Localization.java
+++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java
@@ -213,6 +213,42 @@ public class Localization {
return output;
}
+ /**
+ * Localize an amount of seconds into a human readable string.
+ *
+ * The seconds will be converted to the closest whole time unit.
+ *
For example, 60 seconds would give "1 minute", 119 would also give "1 minute".
+ *
+ * @param context used to get plurals resources.
+ * @param durationInSecs an amount of seconds.
+ * @return duration in a human readable string.
+ */
+ @NonNull
+ public static String localizeDuration(Context context, int durationInSecs) {
+ if (durationInSecs < 0) {
+ throw new IllegalArgumentException("duration can not be negative");
+ }
+
+ final int days = (int) (durationInSecs / (24 * 60 * 60L)); /* greater than a day */
+ durationInSecs %= (24 * 60 * 60L);
+ final int hours = (int) (durationInSecs / (60 * 60L)); /* greater than an hour */
+ durationInSecs %= (60 * 60L);
+ final int minutes = (int) (durationInSecs / 60L);
+ final int seconds = (int) (durationInSecs % 60L);
+
+ final Resources resources = context.getResources();
+
+ if (days > 0) {
+ return resources.getQuantityString(R.plurals.days, days, days);
+ } else if (hours > 0) {
+ return resources.getQuantityString(R.plurals.hours, hours, hours);
+ } else if (minutes > 0) {
+ return resources.getQuantityString(R.plurals.minutes, minutes, minutes);
+ } else {
+ return resources.getQuantityString(R.plurals.seconds, seconds, seconds);
+ }
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// Pretty Time
//////////////////////////////////////////////////////////////////////////*/
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 98264e1bf..b6f73dac7 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -23,6 +23,7 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.about.AboutActivity;
+import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
@@ -343,9 +344,13 @@ public class NavigationHelper {
.commit();
}
- public static void openWhatsNewFragment(FragmentManager fragmentManager) {
+ public static void openFeedFragment(FragmentManager fragmentManager) {
+ openFeedFragment(fragmentManager, FeedGroupEntity.GROUP_ALL_ID, null);
+ }
+
+ public static void openFeedFragment(FragmentManager fragmentManager, long groupId, @Nullable String groupName) {
defaultTransaction(fragmentManager)
- .replace(R.id.fragment_holder, new FeedFragment())
+ .replace(R.id.fragment_holder, FeedFragment.newInstance(groupId, groupName))
.addToBackStack(null)
.commit();
}
diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
index 661aa47c1..bd51919c7 100644
--- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java
@@ -99,6 +99,17 @@ public class ThemeHelper {
return isLightThemeSelected(context) ? R.style.LightDialogTheme : R.style.DarkDialogTheme;
}
+ /**
+ * Return a min-width dialog theme styled according to the (default) selected theme.
+ *
+ * @param context context to get the selected theme
+ * @return the dialog style (the default one)
+ */
+ @StyleRes
+ public static int getMinWidthDialogTheme(Context context) {
+ return isLightThemeSelected(context) ? R.style.LightDialogMinWidthTheme : R.style.DarkDialogMinWidthTheme;
+ }
+
/**
* Return the selected theme styled according to the serviceId.
*
diff --git a/app/src/main/res/drawable/dark_focused_selector.xml b/app/src/main/res/drawable/dark_focused_selector.xml
new file mode 100644
index 000000000..102f40d76
--- /dev/null
+++ b/app/src/main/res/drawable/dark_focused_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dashed_border_black.xml b/app/src/main/res/drawable/dashed_border_black.xml
new file mode 100644
index 000000000..b6bac6252
--- /dev/null
+++ b/app/src/main/res/drawable/dashed_border_black.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dashed_border_dark.xml b/app/src/main/res/drawable/dashed_border_dark.xml
new file mode 100644
index 000000000..5af152ecc
--- /dev/null
+++ b/app/src/main/res/drawable/dashed_border_dark.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dashed_border_light.xml b/app/src/main/res/drawable/dashed_border_light.xml
new file mode 100644
index 000000000..5d29112bd
--- /dev/null
+++ b/app/src/main/res/drawable/dashed_border_light.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_asterisk_black_24dp.xml b/app/src/main/res/drawable/ic_asterisk_black_24dp.xml
new file mode 100644
index 000000000..fa16cd5e8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_asterisk_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_asterisk_white_24dp.xml b/app/src/main/res/drawable/ic_asterisk_white_24dp.xml
new file mode 100644
index 000000000..bd487cb55
--- /dev/null
+++ b/app/src/main/res/drawable/ic_asterisk_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_car_black_24dp.xml b/app/src/main/res/drawable/ic_car_black_24dp.xml
new file mode 100644
index 000000000..6aa8cdd82
--- /dev/null
+++ b/app/src/main/res/drawable/ic_car_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_car_white_24dp.xml b/app/src/main/res/drawable/ic_car_white_24dp.xml
new file mode 100644
index 000000000..7ad263933
--- /dev/null
+++ b/app/src/main/res/drawable/ic_car_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_computer_black_24dp.xml b/app/src/main/res/drawable/ic_computer_black_24dp.xml
new file mode 100644
index 000000000..b03d9c0ce
--- /dev/null
+++ b/app/src/main/res/drawable/ic_computer_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_computer_white_24dp.xml b/app/src/main/res/drawable/ic_computer_white_24dp.xml
new file mode 100644
index 000000000..c4bdad688
--- /dev/null
+++ b/app/src/main/res/drawable/ic_computer_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit_black_24dp.xml b/app/src/main/res/drawable/ic_edit_black_24dp.xml
new file mode 100644
index 000000000..43489826e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit_white_24dp.xml b/app/src/main/res/drawable/ic_edit_white_24dp.xml
new file mode 100644
index 000000000..88f94780f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_emoticon_black_24dp.xml b/app/src/main/res/drawable/ic_emoticon_black_24dp.xml
new file mode 100644
index 000000000..45f489d80
--- /dev/null
+++ b/app/src/main/res/drawable/ic_emoticon_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_emoticon_white_24dp.xml b/app/src/main/res/drawable/ic_emoticon_white_24dp.xml
new file mode 100644
index 000000000..89ca90fb5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_emoticon_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_explore_black_24dp.xml b/app/src/main/res/drawable/ic_explore_black_24dp.xml
new file mode 100644
index 000000000..c898ed9a5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_explore_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_explore_white_24dp.xml b/app/src/main/res/drawable/ic_explore_white_24dp.xml
new file mode 100644
index 000000000..65f2818a6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_explore_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_fastfood_black_24dp.xml b/app/src/main/res/drawable/ic_fastfood_black_24dp.xml
new file mode 100644
index 000000000..fac047550
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fastfood_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_fastfood_white_24dp.xml b/app/src/main/res/drawable/ic_fastfood_white_24dp.xml
new file mode 100644
index 000000000..39bbee49a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fastfood_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_fitness_black_24dp.xml b/app/src/main/res/drawable/ic_fitness_black_24dp.xml
new file mode 100644
index 000000000..40a1cf9c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fitness_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_fitness_white_24dp.xml b/app/src/main/res/drawable/ic_fitness_white_24dp.xml
new file mode 100644
index 000000000..1b2d3b4be
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fitness_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_black_24dp.xml b/app/src/main/res/drawable/ic_heart_black_24dp.xml
new file mode 100644
index 000000000..25cb46e83
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_heart_white_24dp.xml b/app/src/main/res/drawable/ic_heart_white_24dp.xml
new file mode 100644
index 000000000..02c6396ee
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_black_24dp.xml b/app/src/main/res/drawable/ic_help_black_24dp.xml
new file mode 100644
index 000000000..1517747d0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_help_white_24dp.xml b/app/src/main/res/drawable/ic_help_white_24dp.xml
new file mode 100644
index 000000000..d813b72b8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_help_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_kids_black_24dp.xml b/app/src/main/res/drawable/ic_kids_black_24dp.xml
new file mode 100644
index 000000000..d1d8e01e7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_kids_black_24dp.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_kids_white_24dp.xml b/app/src/main/res/drawable/ic_kids_white_24dp.xml
new file mode 100644
index 000000000..c5dda16c8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_kids_white_24dp.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_megaphone_black_24dp.xml b/app/src/main/res/drawable/ic_megaphone_black_24dp.xml
new file mode 100644
index 000000000..21622c162
--- /dev/null
+++ b/app/src/main/res/drawable/ic_megaphone_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_megaphone_white_24dp.xml b/app/src/main/res/drawable/ic_megaphone_white_24dp.xml
new file mode 100644
index 000000000..90e6ff215
--- /dev/null
+++ b/app/src/main/res/drawable/ic_megaphone_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_mic_black_24dp.xml b/app/src/main/res/drawable/ic_mic_black_24dp.xml
new file mode 100644
index 000000000..25d8951a7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_mic_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_mic_white_24dp.xml b/app/src/main/res/drawable/ic_mic_white_24dp.xml
new file mode 100644
index 000000000..36ee9ff81
--- /dev/null
+++ b/app/src/main/res/drawable/ic_mic_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_money_black_24dp.xml b/app/src/main/res/drawable/ic_money_black_24dp.xml
new file mode 100644
index 000000000..4019c2e46
--- /dev/null
+++ b/app/src/main/res/drawable/ic_money_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_money_white_24dp.xml b/app/src/main/res/drawable/ic_money_white_24dp.xml
new file mode 100644
index 000000000..2407a2b73
--- /dev/null
+++ b/app/src/main/res/drawable/ic_money_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml b/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml
new file mode 100644
index 000000000..6009979dd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_motorcycle_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml b/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml
new file mode 100644
index 000000000..b94c29f8f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_motorcycle_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_movie_black_24dp.xml b/app/src/main/res/drawable/ic_movie_black_24dp.xml
new file mode 100644
index 000000000..d70c00f00
--- /dev/null
+++ b/app/src/main/res/drawable/ic_movie_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_movie_white_24dp.xml b/app/src/main/res/drawable/ic_movie_white_24dp.xml
new file mode 100644
index 000000000..f73e76774
--- /dev/null
+++ b/app/src/main/res/drawable/ic_movie_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_music_note_black_24dp.xml b/app/src/main/res/drawable/ic_music_note_black_24dp.xml
new file mode 100644
index 000000000..698159295
--- /dev/null
+++ b/app/src/main/res/drawable/ic_music_note_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_music_note_white_24dp.xml b/app/src/main/res/drawable/ic_music_note_white_24dp.xml
new file mode 100644
index 000000000..1d38e6e22
--- /dev/null
+++ b/app/src/main/res/drawable/ic_music_note_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_people_black_24dp.xml b/app/src/main/res/drawable/ic_people_black_24dp.xml
new file mode 100644
index 000000000..d0fe31838
--- /dev/null
+++ b/app/src/main/res/drawable/ic_people_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_people_white_24dp.xml b/app/src/main/res/drawable/ic_people_white_24dp.xml
new file mode 100644
index 000000000..e6fa4c583
--- /dev/null
+++ b/app/src/main/res/drawable/ic_people_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml
new file mode 100644
index 000000000..f0ff6a871
--- /dev/null
+++ b/app/src/main/res/drawable/ic_person_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_person_white_24dp.xml b/app/src/main/res/drawable/ic_person_white_24dp.xml
new file mode 100644
index 000000000..99f299963
--- /dev/null
+++ b/app/src/main/res/drawable/ic_person_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_pets_black_24dp.xml b/app/src/main/res/drawable/ic_pets_black_24dp.xml
new file mode 100644
index 000000000..b6247bd87
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pets_black_24dp.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_pets_white_24dp.xml b/app/src/main/res/drawable/ic_pets_white_24dp.xml
new file mode 100644
index 000000000..46724a33d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pets_white_24dp.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_radio_black_24dp.xml b/app/src/main/res/drawable/ic_radio_black_24dp.xml
new file mode 100644
index 000000000..00da9101f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_radio_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_radio_white_24dp.xml b/app/src/main/res/drawable/ic_radio_white_24dp.xml
new file mode 100644
index 000000000..df563ec1d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_radio_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml
new file mode 100644
index 000000000..8229a9a64
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml
new file mode 100644
index 000000000..a8175c316
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_restaurant_black_24dp.xml b/app/src/main/res/drawable/ic_restaurant_black_24dp.xml
new file mode 100644
index 000000000..0a8c6bde9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_restaurant_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_restaurant_white_24dp.xml b/app/src/main/res/drawable/ic_restaurant_white_24dp.xml
new file mode 100644
index 000000000..c81618bb7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_restaurant_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_school_black_24dp.xml b/app/src/main/res/drawable/ic_school_black_24dp.xml
new file mode 100644
index 000000000..8f52f0dde
--- /dev/null
+++ b/app/src/main/res/drawable/ic_school_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_school_white_24dp.xml b/app/src/main/res/drawable/ic_school_white_24dp.xml
new file mode 100644
index 000000000..e3888411a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_school_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml b/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml
new file mode 100644
index 000000000..452332095
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shopping_cart_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml b/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml
new file mode 100644
index 000000000..a55bf8a88
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shopping_cart_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sort_black_24dp.xml b/app/src/main/res/drawable/ic_sort_black_24dp.xml
new file mode 100644
index 000000000..fd4c56f0e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sort_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sort_white_24dp.xml b/app/src/main/res/drawable/ic_sort_white_24dp.xml
new file mode 100644
index 000000000..a0c153ad0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sort_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sports_black_24dp.xml b/app/src/main/res/drawable/ic_sports_black_24dp.xml
new file mode 100644
index 000000000..5a54580c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sports_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sports_white_24dp.xml b/app/src/main/res/drawable/ic_sports_white_24dp.xml
new file mode 100644
index 000000000..611852728
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sports_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_stars_black_24dp.xml b/app/src/main/res/drawable/ic_stars_black_24dp.xml
new file mode 100644
index 000000000..66a89110e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_stars_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_stars_white_24dp.xml b/app/src/main/res/drawable/ic_stars_white_24dp.xml
new file mode 100644
index 000000000..2de1fd808
--- /dev/null
+++ b/app/src/main/res/drawable/ic_stars_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sunny_black_24dp.xml b/app/src/main/res/drawable/ic_sunny_black_24dp.xml
new file mode 100644
index 000000000..fee59df13
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sunny_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sunny_white_24dp.xml b/app/src/main/res/drawable/ic_sunny_white_24dp.xml
new file mode 100644
index 000000000..c6cb469ef
--- /dev/null
+++ b/app/src/main/res/drawable/ic_sunny_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_telescope_black_24dp.xml b/app/src/main/res/drawable/ic_telescope_black_24dp.xml
new file mode 100644
index 000000000..9c6132ecc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_telescope_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_telescope_white_24dp.xml b/app/src/main/res/drawable/ic_telescope_white_24dp.xml
new file mode 100644
index 000000000..ea870fd87
--- /dev/null
+++ b/app/src/main/res/drawable/ic_telescope_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_trending_up_black_24dp.xml b/app/src/main/res/drawable/ic_trending_up_black_24dp.xml
new file mode 100644
index 000000000..706af95a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trending_up_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_trending_up_white_24dp.xml b/app/src/main/res/drawable/ic_trending_up_white_24dp.xml
new file mode 100644
index 000000000..403674223
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trending_up_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_videogame_black_24dp.xml b/app/src/main/res/drawable/ic_videogame_black_24dp.xml
new file mode 100644
index 000000000..df872c96c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_videogame_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_videogame_white_24dp.xml b/app/src/main/res/drawable/ic_videogame_white_24dp.xml
new file mode 100644
index 000000000..593e49e14
--- /dev/null
+++ b/app/src/main/res/drawable/ic_videogame_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_watch_later_black_24dp.xml b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml
new file mode 100644
index 000000000..5a1b9ac74
--- /dev/null
+++ b/app/src/main/res/drawable/ic_watch_later_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_watch_later_white_24dp.xml b/app/src/main/res/drawable/ic_watch_later_white_24dp.xml
new file mode 100644
index 000000000..f9fffbc43
--- /dev/null
+++ b/app/src/main/res/drawable/ic_watch_later_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_work_black_24dp.xml b/app/src/main/res/drawable/ic_work_black_24dp.xml
new file mode 100644
index 000000000..2668f2c43
--- /dev/null
+++ b/app/src/main/res/drawable/ic_work_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_work_white_24dp.xml b/app/src/main/res/drawable/ic_work_white_24dp.xml
new file mode 100644
index 000000000..8a1db7828
--- /dev/null
+++ b/app/src/main/res/drawable/ic_work_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_world_black_24dp.xml b/app/src/main/res/drawable/ic_world_black_24dp.xml
new file mode 100644
index 000000000..48785e7d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_world_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_world_white_24dp.xml b/app/src/main/res/drawable/ic_world_white_24dp.xml
new file mode 100644
index 000000000..01583e467
--- /dev/null
+++ b/app/src/main/res/drawable/ic_world_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/light_focused_selector.xml b/app/src/main/res/drawable/light_focused_selector.xml
new file mode 100644
index 000000000..102f40d76
--- /dev/null
+++ b/app/src/main/res/drawable/light_focused_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_feed_group_create.xml b/app/src/main/res/layout/dialog_feed_group_create.xml
new file mode 100644
index 000000000..364a6c891
--- /dev/null
+++ b/app/src/main/res/layout/dialog_feed_group_create.xml
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_feed_group_reorder.xml b/app/src/main/res/layout/dialog_feed_group_reorder.xml
new file mode 100644
index 000000000..82a9b1591
--- /dev/null
+++ b/app/src/main/res/layout/dialog_feed_group_reorder.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feed_group_add_new_item.xml b/app/src/main/res/layout/feed_group_add_new_item.xml
new file mode 100644
index 000000000..3424762e2
--- /dev/null
+++ b/app/src/main/res/layout/feed_group_add_new_item.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feed_group_card_item.xml b/app/src/main/res/layout/feed_group_card_item.xml
new file mode 100644
index 000000000..b6bf8656b
--- /dev/null
+++ b/app/src/main/res/layout/feed_group_card_item.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feed_group_reorder_item.xml b/app/src/main/res/layout/feed_group_reorder_item.xml
new file mode 100644
index 000000000..d3bbf8005
--- /dev/null
+++ b/app/src/main/res/layout/feed_group_reorder_item.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feed_import_export_group.xml b/app/src/main/res/layout/feed_import_export_group.xml
new file mode 100644
index 000000000..2049db65e
--- /dev/null
+++ b/app/src/main/res/layout/feed_import_export_group.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feed_item_carousel.xml b/app/src/main/res/layout/feed_item_carousel.xml
new file mode 100644
index 000000000..db3d9cb11
--- /dev/null
+++ b/app/src/main/res/layout/feed_item_carousel.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml
index 71217eea3..7d166a3f5 100644
--- a/app/src/main/res/layout/fragment_feed.xml
+++ b/app/src/main/res/layout/fragment_feed.xml
@@ -1,18 +1,116 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:visibility="gone"
+ tools:listitem="@layout/list_stream_item"
+ tools:visibility="visible" />
+
+
+
+
+
+
+
+ tools:visibility="visible" />
+ tools:visibility="visible" />
-
+ android:layout_alignParentTop="true"
+ android:background="?attr/toolbar_shadow_drawable" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_subscription.xml b/app/src/main/res/layout/fragment_subscription.xml
index 979993a56..d1281f462 100644
--- a/app/src/main/res/layout/fragment_subscription.xml
+++ b/app/src/main/res/layout/fragment_subscription.xml
@@ -1,7 +1,6 @@
@@ -9,11 +8,10 @@
+ tools:listitem="@layout/list_channel_item"/>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/header_item.xml b/app/src/main/res/layout/header_item.xml
new file mode 100644
index 000000000..4d4e1b884
--- /dev/null
+++ b/app/src/main/res/layout/header_item.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/header_with_menu_item.xml b/app/src/main/res/layout/header_with_menu_item.xml
new file mode 100644
index 000000000..580e8db4d
--- /dev/null
+++ b/app/src/main/res/layout/header_with_menu_item.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/header_with_text_item.xml b/app/src/main/res/layout/header_with_text_item.xml
new file mode 100644
index 000000000..871893ad6
--- /dev/null
+++ b/app/src/main/res/layout/header_with_text_item.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_channel_grid_item.xml b/app/src/main/res/layout/list_channel_grid_item.xml
index 3fe642974..423bfeb9e 100644
--- a/app/src/main/res/layout/list_channel_grid_item.xml
+++ b/app/src/main/res/layout/list_channel_grid_item.xml
@@ -1,48 +1,48 @@
-
+
-
+
-
+
-
+
diff --git a/app/src/main/res/layout/list_empty_view.xml b/app/src/main/res/layout/list_empty_view.xml
index e1833f243..094353324 100644
--- a/app/src/main/res/layout/list_empty_view.xml
+++ b/app/src/main/res/layout/list_empty_view.xml
@@ -3,7 +3,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="128dp"
android:gravity="center"
android:orientation="vertical">
diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml
index 02e8f1531..d2000381d 100644
--- a/app/src/main/res/layout/list_stream_item.xml
+++ b/app/src/main/res/layout/list_stream_item.xml
@@ -75,6 +75,7 @@
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/itemThumbnailView"
android:layout_toEndOf="@+id/itemThumbnailView"
+ android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml
index 2747038f6..00b431cc6 100644
--- a/app/src/main/res/layout/list_stream_playlist_item.xml
+++ b/app/src/main/res/layout/list_stream_playlist_item.xml
@@ -78,6 +78,7 @@
android:layout_toStartOf="@id/itemHandle"
android:layout_toRightOf="@+id/itemThumbnailView"
android:layout_toEndOf="@+id/itemThumbnailView"
+ android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_uploader_text_size"
diff --git a/app/src/main/res/layout/picker_icon_item.xml b/app/src/main/res/layout/picker_icon_item.xml
new file mode 100644
index 000000000..f156772b6
--- /dev/null
+++ b/app/src/main/res/layout/picker_icon_item.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/picker_subscription_item.xml b/app/src/main/res/layout/picker_subscription_item.xml
new file mode 100644
index 000000000..474f068df
--- /dev/null
+++ b/app/src/main/res/layout/picker_subscription_item.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/subscription_header.xml b/app/src/main/res/layout/subscription_header.xml
index 821e1b2f4..9deabada0 100644
--- a/app/src/main/res/layout/subscription_header.xml
+++ b/app/src/main/res/layout/subscription_header.xml
@@ -7,37 +7,6 @@
android:orientation="vertical"
android:paddingBottom="12dp">
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 5fdb8e1f5..1e48f1800 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -62,7 +62,7 @@
مشترك
الرئيسية
الاشتراكات
- ما الجديد
+ ما الجديد
في الخلفية
تشغيل تلقائي
اسود
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index d39e46d5a..bf3b0f3f2 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -27,7 +27,7 @@
Abunəliklər
Əlfəcinlər
- Yeni nə var
+ Yeni nə var
Arxa fon
Video yükləmə ünvanı
diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml
index 721d0a7eb..347afa2cf 100644
--- a/app/src/main/res/values-b+ast/strings.xml
+++ b/app/src/main/res/values-b+ast/strings.xml
@@ -74,7 +74,7 @@
Soscribise
Nun pudo anovase la soscripción
Soscripciones
- Novedaes
+ Novedaes
Historial de gueta
Sigue cola reproducción dempués de les interrupciones (llamaes telefóniques, por exemplu)
diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
index 9c4739e41..fe866fd04 100644
--- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml
+++ b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
@@ -233,7 +233,7 @@
无法更新订阅
主页
订阅
- 最新
+ 最新
恢复前台焦点
中断后继续播放(例如突然来电后)
搜索历史记录
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index b3a09cb8f..58ea028df 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -31,7 +31,7 @@
Галоўная
Падпіскі
Адзначаныя плэйлісты
- Што новага
+ Што новага
У фоне
У акне
Дадаць да
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 7c813d0a9..26d394254 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -26,7 +26,7 @@
Неуспешна промяна на абонамента
Неуспешно обновление на абонамента
Абонаменти
- Обновления
+ Обновления
Във фонов режим
В прозорец
Директория за изтегляне на видео
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index 60b87caa9..6ba96425a 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -144,7 +144,7 @@
কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর:
স্ট্রিম ফাইল ডাউনলোড করুন।
তথ্য দেখুন
- কি নতুন
+ কি নতুন
যুক্ত করুন
খোজ ইতিহাস
ইতিহাস
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 20deb3160..8bc945992 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -13,7 +13,7 @@
Mostra la informació
Subscripcions
Llistes de reproducció desades
- Novetats
+ Novetats
Carpeta de baixada dels vídeos
Carpeta de baixada dels fitxers d\'àudio
Reproducció automàtica
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index f8ac19ddd..9fd68d180 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -133,7 +133,7 @@ otevření ve vyskakovacím okně
Nelze aktualizovat odběr
Hlavní
Odběry
- Co je nového
+ Co je nového
Na pozadí
V okně
Výchozí rozlišení vyskakovacího okna
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index ff2fea8c9..25abe3d1a 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -34,7 +34,7 @@
Gemte spillelister
Ny fane
Vælg fane
- Nyheder
+ Nyheder
Baggrund
Pop op
Føj til
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index d8ad7169b..1d69e7a82 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -168,7 +168,7 @@
Abonniert
Kanal abbestellt
Abos
- Neuigkeiten
+ Neuigkeiten
Suchverlauf
Suchanfragen lokal speichern
Verlauf ansehen
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 915e1902d..e67bea60d 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -100,7 +100,7 @@
Κύριο
Συνδρομές
Αγαπημένες λίστες αναπαραγωγής
- Νέα
+ Νέα
Στο παρασκήνιο
Αναδυόμενο παράθυρο
Προσθήκη σε
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index fc57b106c..6af9723f5 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -81,7 +81,7 @@
Ĉefa
Abonoj
Konservitaj ludlistoj
- Kio novas
+ Kio novas
Fono
Ŝprucfenestro
Aldonu al
@@ -120,7 +120,7 @@
Jes
Poste
- Tiu permeso estas necesa por
+ Tiu permeso estas necesa por
\nmalfermi en ŝprucfenestra modo
Ludante en ŝprucfenestra modo
Malŝatitaj
@@ -211,15 +211,15 @@
Tia dosiero/enhavo ne ekzistas
Dosiernomo ne povas esti malplena
Eraro okazis: %1$s
- Importu Jutubajn abonaĵojn per elŝuti la dosieron de eksporto :
-\n
-\n1. Iru ĉe tiu retpaĝo: %1$s
-\n2. Ensalutu kiam oni petas vin
+ Importu Jutubajn abonaĵojn per elŝuti la dosieron de eksporto :
+\n
+\n1. Iru ĉe tiu retpaĝo: %1$s
+\n2. Ensalutu kiam oni petas vin
\n3. Elŝuto devus komenci (ĝi estas la dosiero de eksporto)
- Importu Soundcloud-n profilon tajpante ĉu la ligilon, ĉu vian ID :
+ Importu Soundcloud-n profilon tajpante ĉu la ligilon, ĉu vian ID :
\n
-\n1. Ebligu komputilon modon en retumilon (la retejo malhaveblas por poŝtelefonoj)
-\n2. Iru tien: %1$s
+\n1. Ebligu komputilon modon en retumilon (la retejo malhaveblas por poŝtelefonoj)
+\n2. Iru tien: %1$s
\n3. Ensalutu kiam oni petas vin
\n4. Kopiu la ligilon de profilo ke oni kondikis vin.
Malŝaltu por malebligi ŝarĝajn bildetojn, konservi datumojn kaj uzadon de memoro. Ŝanĝoj forviŝas ambaŭ en-memoro kaj sur-disko bildo kaŝmemoro.
@@ -291,7 +291,7 @@
Oni petos vin kie konservi ĉion elŝutaĵon.
\nElektu AFM se vi volas elŝuti al ekstera SD-karto
Uzu AFM
- La \"Atinga Framo al la Memoro\" ebligas elŝuti al ekstera SD-karto.
+ La \"Atinga Framo al la Memoro\" ebligas elŝuti al ekstera SD-karto.
\nKomento: kelkaj aparatoj malkongruas
Forviŝi ludajn poziciojn
Forviŝi la totalon de ludaj pozicioj
@@ -440,10 +440,10 @@
NewPipe estas programaro sub rajtoceda permesilo: Vi povas uzi, studi, komuniki kaj plibonigi ĝin kiel vi volas. Precize, vi povas redistribui kaj/aŭ modifi ĝin sub la kondiĉoj de la Ĝenerala Publika Permesilo de GNU, kiel publikigita per la Free Software Foundation, ĉu en la versio 3, ĉu (se vi volas) ajna posta versio.
Ĉu vi volas ankaŭ importi agordojn\?
Privateca politiko de NewPipe
- La NewPipe projekto serioze respektas vian privatecon. Konsekvence, la apo ne kolektas ajnan datumon sen via konsento.
+ La NewPipe projekto serioze respektas vian privatecon. Konsekvence, la apo ne kolektas ajnan datumon sen via konsento.
\nLa privateco politiko de NewPipe detale eksplikas kion datumon estas sendita kaj stokita kiam vi sendas falegosignalon.
Legi la privatecan politikon
- Por konformiĝi al la Ĝenerala Datum-Protekta Regularon (GDPR), ni allogas vian atenton al la privateca politiko de NewPipe. Bonvolu atentive legi ĝin.
+ Por konformiĝi al la Ĝenerala Datum-Protekta Regularon (GDPR), ni allogas vian atenton al la privateca politiko de NewPipe. Bonvolu atentive legi ĝin.
\nVi devas akcepti ĝin por sendi la cimsignalon al ni.
Akcepti
Rifuzi
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 3cc903e10..b6e0386a3 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -166,7 +166,7 @@
No se pudo actualizar la suscripción
Principal
Suscripciones
- Qué hay de nuevo
+ Qué hay de nuevo
Reanudar reproducción
Continuar reproduciendo después de las interrupciones (ej. llamadas telefónicas)
Descargar
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 79442f421..6521c1d45 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -30,7 +30,7 @@
Kuva info
Tellimused
Esitusloendid järjehoidjates
- Mis on uut
+ Mis on uut
Taust
Hüpikaken
"Lisa "
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index f0fab60ec..e8bd8a9a7 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -165,7 +165,7 @@
Ezin izan da harpidetza eguneratu
Nagusia
Harpidetzak
- Zer dago berri
+ Zer dago berri
Jarraitu erreprodukzioa
Jarraitu etenaldiak eta gero (adib. telefono deiak)
Deskargak
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 790c47669..36e3524c7 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -115,7 +115,7 @@
اصلی
اشتراکها
فهرستهای پخش دارای نشانک
- موارد جدید
+ موارد جدید
پس زمینه
افزودن به
نمایش کیفیت بالاتر
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index d34a6408a..46a47c6b5 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -27,7 +27,7 @@
Ei pystytty päivittämään tilausta
Päävalikko
Tilaukset
- Uudet
+ Uudet
Taustatoisto
Ikkuna
Videolatausten sijainti
diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml
index 9813b6922..d52b1f4b9 100644
--- a/app/src/main/res/values-fil/strings.xml
+++ b/app/src/main/res/values-fil/strings.xml
@@ -34,7 +34,7 @@
Naka-bookmark mga Playlist
Bagong Tab
Pumili nang Tab
- Anong Bago
+ Anong Bago
Likuran
Popup
Idagdag sa
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 10e31e72f..c41d5c54f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -147,7 +147,7 @@
Désabonné de la chaîne
Principal
Abonnements
- Nouveautés
+ Nouveautés
Téléchargement
Paramètres
À propos
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 6427e1378..af1066152 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -32,7 +32,7 @@
Subscricións
Favoritos
- Novidades
+ Novidades
Segundo plano
Modo popup
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 5de0e7816..a3f4583cb 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -105,7 +105,7 @@
לא ניתן לעדכן את המינוי
ראשי
מינויים
- מה חדש
+ מה חדש
היסטוריית חיפוש
שמירת שאילתות החיפוש מקומית
היסטוריית צפייה
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index a7746330a..f08274263 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -62,7 +62,7 @@
मुख्य
सदस्यता नहीं बदला जा सका
सदस्यता का अद्यतन नहीं हो सका
- देखे की क्या नया है
+ देखे की क्या नया है
वीडियो डाउनलोड फ़ोल्डर
डाउनलोड की गई वीडियो फ़ाइलें यहां संग्रहीत हैं
वीडियो फ़ाइलों के लिए डाउनलोड फ़ोल्डर चुनें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 204d8895c..e3da3e711 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -27,7 +27,7 @@
Nije moguće osvježiti pretplatu
Početna
Pretplate
- Što je novo
+ Što je novo
Pozadina
Skočni prozor
Mapa za preuzimanje videozapisa
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 1fda2a515..1f74f79e6 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -109,7 +109,7 @@
Főoldal
Feliratkozások
Könyvjelzőzött lejátszási listák
- Újdonságok
+ Újdonságok
Háttér
Felugró ablak
Nem található stream lejátszó alkalmazás (feltelepítheted a VLC-t a lejátszáshoz).
diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml
index ff90b50ab..822d714f6 100644
--- a/app/src/main/res/values-ia/strings.xml
+++ b/app/src/main/res/values-ia/strings.xml
@@ -17,7 +17,7 @@
Subscriptiones
Nove scheda
Seliger le scheda
- Novitates
+ Novitates
Fundo
Emergente
Adder a
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 54581e9ac..b4db9141d 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -165,7 +165,7 @@
Kontribusi
Subscribe
Disubscribe
- Apa Yang Baru
+ Apa Yang Baru
Lanjutkan pemutaran
Melanjutkan pemutaran setelah interupsi (mis. panggilan telepon)
Utama
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 37e328799..1ad3bc218 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -164,7 +164,7 @@
Impossibile cambiare l\'iscrizione
Impossibile aggiornare l\'iscrizione
Iscrizioni
- Novità
+ Novità
Cronologia Ricerche
Salva le ricerche localmente
Cronologia Visualizzazioni
@@ -381,7 +381,7 @@
NewPipe è un software libero con licenza copyleft: si può utilizzare, studiare, condividere e migliorare a proprio piacimento. In particolare, è possibile ridistribuirlo e/o modificarlo secondo i termini della GNU General Public License (Free Software Foundation), nella versione 3 o successiva, a propria discrezione.
Vuoi anche importare le impostazioni?
Informativa sulla Privacy
- Il progetto NewPipe tiene molto alla tua privacy. Perciò, l\'app non raccoglie alcun dato senza il tuo consenso.
+ Il progetto NewPipe tiene molto alla tua privacy. Perciò, l\'app non raccoglie alcun dato senza il tuo consenso.
\nL\'informativa sulla privacy spiega nel dettaglio quali dati vengono trattati e memorizzati durante l\'invio di segnalazioni per arresti anomali.
Leggi l\'informativa sulla privacy
Per rispettare il regolamento europeo sulla protezione dei dati (GDPR), attiriamo la vostra attenzione riguardo l\'informativa sulla privacy di NewPipe. Si prega di leggerla attentamente.
@@ -496,9 +496,9 @@
Ogni volta verrà chiesta la destinazione dei file
Utilizza SAF
Limita Coda Download
- Ogni volta verrà chiesta la destinazione dei file.
+ Ogni volta verrà chiesta la destinazione dei file.
\nScegli SAF se vuoi scaricare su una scheda SD esterna
- \"Storage Access Framework\" consente di salvare file su una memoria esterna.
+ \"Storage Access Framework\" consente di salvare file su una memoria esterna.
\nAlcuni dispositivi non sono compatibili
Elimina posizioni di riproduzione
Elimina tutte le posizioni di riproduzione
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 3fcef7899..16ee3ed03 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -165,7 +165,7 @@
チャンネル登録を更新できません
メイン
登録リスト
- 新着
+ 新着
検索履歴
検索した履歴を記憶します
視聴履歴
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 54437a375..4935c4bdb 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -98,7 +98,7 @@
구독을 업데이트할 수 없음
메인 화면
구독
- 새로운 영상
+ 새로운 영상
백그라운드
팝업
기본 팝업 해상도
diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml
index 3ac62a30a..43b32a29d 100644
--- a/app/src/main/res/values-ku/strings.xml
+++ b/app/src/main/res/values-ku/strings.xml
@@ -32,7 +32,7 @@
سهرهكی
بهشدارییهكان
خشتەی کارپێکردنەکان نیشانەکران
- چی نوێ ههیه
+ چی نوێ ههیه
لە پاشبنەما
پهنجهرهی بچووک
زیادکردن بۆ
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index f84a3c23c..fef2e3549 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -166,7 +166,7 @@
Pagrindinis
Prenumeratos
- Kas Naujo
+ Kas Naujo
Ieškoti istorijoje
Saugoti paieškos užklausas vietinėje atmintyje
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 96b4c8819..8bf86c102 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -31,7 +31,7 @@
Почетна
Членства
Обележани плејлисти
- Новости
+ Новости
Позадина
Подпрозорче
Додај на
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index d8d291759..eb8826d45 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -34,7 +34,7 @@
Henti langganan
Tab Baru
Pilih Tab
- Apa yang Baru
+ Apa yang Baru
Latar Belakang
Popup
Tambahkan Ke
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index e0392a72d..95a19c903 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -134,7 +134,7 @@
Klarte ikke å endre abonnement
Klarte ikke å oppdatere abonnement
Abonnementer
- Hva er nytt
+ Hva er nytt
Bakgrunn
Oppsprett
Husk oppsprettsstørrelse og posisjon
diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml
index de96152ec..4c3b9628d 100644
--- a/app/src/main/res/values-nl-rBE/strings.xml
+++ b/app/src/main/res/values-nl-rBE/strings.xml
@@ -31,7 +31,7 @@
Start
Abonnementen
Bijgehouden afspeellijsten
- Nieuw
+ Nieuw
Achtergrond
Pop-up
Toevoegen aan
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 9bde969cb..35faa9dde 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -171,7 +171,7 @@
Kan abonnement niet bijwerken
Startpagina
Abonnementen
- Nieuw
+ Nieuw
Zoekgeschiedenis
Sla zoekopdrachten lokaal op
Geschiedenis en cache
diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml
index e69e43b44..937f76df4 100644
--- a/app/src/main/res/values-oc/strings.xml
+++ b/app/src/main/res/values-oc/strings.xml
@@ -34,7 +34,7 @@
Listas de lectura enregistradas
Onglet novèl
Causir un onglet
- Çò novèl
+ Çò novèl
Rèireplan
Fenestron
Apondre a
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 7e39321e0..29d030e1e 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -31,7 +31,7 @@
ਸਬਸਕ੍ਰਿਪਸ਼ਨ ਅੱਪਡੇਟ ਕਰਨ ਵਿਚ ਅਸਮਰੱਥ
ਸਬਸਕ੍ਰਿਪਸ਼ਨ
ਬੁੱਕਮਾਰਕ ਪਲੇਲਿਸਟਾਂ
- ਨਵਾਂ ਕੀ ਹੈ
+ ਨਵਾਂ ਕੀ ਹੈ
ਬੈਕਗਰਾਊਂਡ
ਪੋਪ-ਅਪ
ਸ਼ਾਮਿਲ ਕਰੋ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index ded3ff417..694c29554 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -169,7 +169,7 @@
Nie można zaktualizować subskrypcji
Główna
Subskrypcje
- Co nowego
+ Co nowego
Historia wyszukiwania
Zapisuj lokalnie historię wyszukiwania
Historia oglądanych
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index fad9fea06..c96435799 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -172,7 +172,7 @@ abrir em modo popup
Não foi possível atualizar inscrição
Principal
Inscrições
- Novidades
+ Novidades
Retomar reprodução
Continuar reproduzindo depois de interrupções (por exemplo: ligações)
Histórico de pesquisas
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index c06a1909d..504b98642 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -165,7 +165,7 @@
Não foi possível atualizar a subscrição
Principal
Subscrições
- Novidades
+ Novidades
Histórico de pesquisa
Guardar termos de pesquisa localmente
Ver histórico
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index d69ad016e..61d6a2d9a 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -149,7 +149,7 @@ pentru a deschide în mod pop-up
Nu s-a putut actualiza abonamentul
Principal
Abonamente
- Ce este nou
+ Ce este nou
Istoric de căutări
Stochează local căutările
Istoric vizualizări.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 0fb62f073..a0d292c80 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -163,7 +163,7 @@
Не удалось обновить подписку
Главная
Подписки
- Что нового
+ Что нового
История поиска
Хранить запросы поиска локально
История просмотров
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index a6a20a48f..bdda945bc 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -149,7 +149,7 @@
Nemožno aktualizovať odber
Hlavné
Odbery
- Čo je nové
+ Čo je nové
Hľadať v histórií
Hľadané výrazy ukladať lokálne
História pozretí
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index f318ca77e..d3adc9eca 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -201,7 +201,7 @@ odpiranje v pojavnem načinu
Glavno
Naročnine
- Kaj je novega
+ Kaj je novega
Prejmi
Dovoljeni znaki v imenih datotek
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 74bf10804..1ef4bb6f5 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -14,7 +14,7 @@
Shfaq informatat
Menyja kryesore
- Të rejat
+ Të rejat
Shto në
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index ee999d779..79dccafc5 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -145,7 +145,7 @@
Претплати
Главно
Претплате
- Шта је ново
+ Шта је ново
Историјат претраге
Уписуј појмове претраге локално
Историјат
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 5290517ab..f95944aee 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -106,7 +106,7 @@
Kunde inte uppdatera prenumeration
Hem
Prenumerationer
- Vad är nytt
+ Vad är nytt
Spela upp automatiskt
Sökhistorik
Spara sökfrågor lokalt
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 41e683545..409cf5d12 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -30,7 +30,7 @@
முதன்மை
குழுசேர்ப்புகள்
குறிக்கப்பட்ட காணொலி பட்டியல்கள்
- புதிதாக
+ புதிதாக
பின்னால்
திரைமேல்
சேர்
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index 17519e084..e7e87e935 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -24,7 +24,7 @@
సబ్ స్క్రైబ్ నవీకరించలేరు
ప్రధానంగా
సభ్యత్వం
- కొత్తది ఏమిటి
+ కొత్తది ఏమిటి
వెనకవైపు
పాపప్
వీడియో డౌన్లోడ్ మార్గం
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 0fc066986..b4f506158 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -33,7 +33,7 @@
เพลย์ลิสต์ที่เก็บไว้
แท็บใหม่
เลือกแท็บ
- มีอะไรใหม่
+ มีอะไรใหม่
พื้นหลัง
ป๊อปอัพ
เพิ่มไปยัง
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 78d352e82..05bc3cbe7 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -171,7 +171,7 @@
Abonelik güncellenemedi
Ana
Abonelikler
- Yenilikler
+ Yenilikler
Arama geçmişi
Arama sorgularını yerel olarak saklayın
İzleme geçmişi
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index ad4e47312..0d0c6082c 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -119,7 +119,7 @@
Не вдалося оновити підписку
Головна
Підписки
- Новинки
+ Новинки
У тлі
У вікні
Типова роздільна здатність вікна
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 627def4d3..2c712d878 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -31,7 +31,7 @@
مرکزی
رکنیتیں
نشان زدہ پلے لسٹس
- نیا کیا ہے
+ نیا کیا ہے
پس منظر
پوپ اپ
شامل کریں
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index bfde056b0..0f6cfe96f 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -173,7 +173,7 @@
Trang chủ
Đăng ký
Danh sách phát được đánh dấu
- Có gì mới
+ Có gì mới
Thêm vào
Sử dụng tìm kiếm nhanh không chính xác
Tìm kiếm không chính xác cho phép trình phát tìm đến vị trí nhanh hơn với độ chính xác bị hạn chế
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 8be5dd967..8d210c067 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -237,7 +237,7 @@
无法更新订阅
主页
订阅
- 最新
+ 最新
恢复前台焦点
中断后继续播放(例如突然来电后)
搜索历史记录
@@ -411,7 +411,7 @@
NewPipe 项目非常重视您的隐私。因此,未经您的同意,应用程序不会收集任何数据。
\nNewPipe 的隐私政策详细解释了在发送崩溃报告时发送和存储的数据。
阅读隐私政策
- 为了遵守欧盟的《通用数据保护条例》(GDPR),我们特此提醒您注意 NewPipe 的隐私政策。请您仔细阅读。
+ 为了遵守欧盟的《通用数据保护条例》(GDPR),我们特此提醒您注意 NewPipe 的隐私政策。请您仔细阅读。
\n您必须在同意以后才能向我们发送错误报告。
接受
拒绝
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index c141b2c94..2837990ac 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -148,7 +148,7 @@
無法更新訂閱
主頁
訂閱清單
- 新鮮事
+ 新鮮事
搜尋紀錄
在本機儲存搜尋紀錄
檢視歷史
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 88925a598..4cda52a99 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -41,21 +41,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 5741d1b4f..425886cdc 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -14,6 +14,9 @@
#ffffff
#212121
#c8ffffff
+ #F8F8F8
+ #E9E9E9
+ #33000000
#222222
@@ -27,12 +30,18 @@
#424242
#ffffff
#af000000
+ #313131
+ #474747
+ #33FFFFFF
#000000
@color/dark_settings_accent_color
#1effffff
#23454545
+ #0F0F0F
+ #202020
+ #25FFFFFF
#ddffffff
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 582c4bade..538179b73 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -14,6 +14,9 @@
70dp
164dp
92dp
+
+ 42dp
+ 128dp
96dp
@@ -24,10 +27,12 @@
2dp
4dp
8dp
+ 12dp
180dp
150dp
+ 32dp
42dp
24dp
@@ -89,4 +94,10 @@
12sp
12sp
+
+
+ 12dp
+ 2dp
+ 4dp
+
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index fe096c9fd..e97bf11ca 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -9,6 +9,7 @@
@string/youtube
saved_tabs_key
+ feed_last_updated
download_path
@@ -180,6 +181,31 @@
app_language_key
enable_lock_screen_video_thumbnail
+ feed_update_threshold_key
+ 300
+
+
+
+ - @string/feed_update_threshold_option_always_update
+ - 5 minutes
+ - 15 minutes
+ - 1 hour
+ - 6 hours
+ - 12 hours
+ - 1 day
+
+
+
+ - 0
+ - 300
+ - 900
+ - 3600
+ - 21600
+ - 43200
+ - 86400
+
+ feed_use_dedicated_fetch_method
+
import_data
export_data
@@ -1082,12 +1108,17 @@
list_view_mode
- auto
+ @string/list_view_mode_auto_key
+
+
+ auto
+ list
+ grid
- - auto
- - list
- - grid
+ - @string/list_view_mode_auto_key
+ - @string/list_view_mode_list_key
+ - @string/list_view_mode_grid_key
- @string/auto
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 48e8a4854..a1e9d28d1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -36,7 +36,6 @@
Bookmarked Playlists
New Tab
Choose Tab
- What\'s New
Background
Popup
Add To
@@ -192,6 +191,7 @@
Deletes history of search keywords
Delete entire search history?
Search history deleted.
+ Help
Error
External storage unavailable
@@ -598,4 +598,50 @@
- %s second
- %s seconds
-
+
+
+
+ - %d second
+ - %d seconds
+
+
+
+ - %d minute
+ - %d minutes
+
+
+
+ - %d hour
+ - %d hours
+
+
+
+ - %d day
+ - %d days
+
+
+
+ What\'s New
+ Feed groups
+ Oldest subscription update: %s
+ Not loaded: %d
+ Loading feed…
+ Processing feed…
+ Select subscriptions
+ Selected: %d
+ No subscription selected
+ Empty group name
+ Name
+ Do you want to delete this group?
+ New
+
+ Feed
+ Feed update threshold
+ Time after last update before a subscription is considered outdated — %s
+ Always update
+ Fetch from dedicated feed when available
+ Available in some services, it is usually much faster but may return a limited amount of items and often incomplete information (e.g. no duration, item type, no live status).
+ Enable fast mode
+ Disable fast mode
+ Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information.
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d72a1b092..f7d09ef9c 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -61,15 +61,55 @@
- @drawable/ic_delete_black_24dp
- @drawable/ic_settings_update_black
- @drawable/ic_done_black_24dp
+ - @drawable/ic_refresh_black_24dp
+ - @drawable/ic_computer_black_24dp
+ - @drawable/ic_videogame_black_24dp
+ - @drawable/ic_music_note_black_24dp
+ - @drawable/ic_stars_black_24dp
+ - @drawable/ic_sports_black_24dp
+ - @drawable/ic_money_black_24dp
+ - @drawable/ic_person_black_24dp
+ - @drawable/ic_people_black_24dp
+ - @drawable/ic_heart_black_24dp
+ - @drawable/ic_kids_black_24dp
+ - @drawable/ic_fastfood_black_24dp
+ - @drawable/ic_car_black_24dp
+ - @drawable/ic_motorcycle_black_24dp
+ - @drawable/ic_trending_up_black_24dp
+ - @drawable/ic_school_black_24dp
+ - @drawable/ic_asterisk_black_24dp
+ - @drawable/ic_emoticon_black_24dp
+ - @drawable/ic_edit_black_24dp
+ - @drawable/ic_explore_black_24dp
+ - @drawable/ic_fitness_black_24dp
+ - @drawable/ic_restaurant_black_24dp
+ - @drawable/ic_mic_black_24dp
+ - @drawable/ic_radio_black_24dp
+ - @drawable/ic_shopping_cart_black_24dp
+ - @drawable/ic_watch_later_black_24dp
+ - @drawable/ic_work_black_24dp
+ - @drawable/ic_movie_black_24dp
+ - @drawable/ic_pets_black_24dp
+ - @drawable/ic_world_black_24dp
+ - @drawable/ic_sunny_black_24dp
+ - @drawable/ic_telescope_black_24dp
+ - @drawable/ic_megaphone_black_24dp
+ - @drawable/ic_sort_black_24dp
+ - @drawable/ic_help_black_24dp
- @color/light_separator_color
- @color/light_contrast_background_color
- @drawable/light_checked_selector
+ - @drawable/light_focused_selector
- @color/light_queue_background_color
- @drawable/toolbar_shadow_light
- @drawable/light_selector
- @color/light_ripple_color
- @drawable/progress_youtube_horizontal_light
+ - @color/light_card_item_background_color
+ - @color/light_card_item_contrast_color
+ - @color/light_border_color
+ - @drawable/dashed_border_light
- @style/PreferenceThemeOverlay.v14.Material
@@ -127,15 +167,55 @@
- @drawable/ic_pause_white_24dp
- @drawable/ic_settings_update_white
- @drawable/ic_done_white_24dp
+ - @drawable/ic_refresh_white_24dp
+ - @drawable/ic_computer_white_24dp
+ - @drawable/ic_videogame_white_24dp
+ - @drawable/ic_music_note_white_24dp
+ - @drawable/ic_stars_white_24dp
+ - @drawable/ic_sports_white_24dp
+ - @drawable/ic_money_white_24dp
+ - @drawable/ic_person_white_24dp
+ - @drawable/ic_people_white_24dp
+ - @drawable/ic_heart_white_24dp
+ - @drawable/ic_kids_white_24dp
+ - @drawable/ic_fastfood_white_24dp
+ - @drawable/ic_car_white_24dp
+ - @drawable/ic_motorcycle_white_24dp
+ - @drawable/ic_trending_up_white_24dp
+ - @drawable/ic_school_white_24dp
+ - @drawable/ic_asterisk_white_24dp
+ - @drawable/ic_emoticon_white_24dp
+ - @drawable/ic_edit_white_24dp
+ - @drawable/ic_explore_white_24dp
+ - @drawable/ic_fitness_white_24dp
+ - @drawable/ic_restaurant_white_24dp
+ - @drawable/ic_mic_white_24dp
+ - @drawable/ic_radio_white_24dp
+ - @drawable/ic_shopping_cart_white_24dp
+ - @drawable/ic_watch_later_white_24dp
+ - @drawable/ic_work_white_24dp
+ - @drawable/ic_movie_white_24dp
+ - @drawable/ic_pets_white_24dp
+ - @drawable/ic_world_white_24dp
+ - @drawable/ic_sunny_white_24dp
+ - @drawable/ic_telescope_white_24dp
+ - @drawable/ic_megaphone_white_24dp
+ - @drawable/ic_sort_white_24dp
+ - @drawable/ic_help_white_24dp
- @color/dark_separator_color
- @color/dark_contrast_background_color
- @drawable/dark_checked_selector
+ - @drawable/dark_focused_selector
- @color/dark_queue_background_color
- @drawable/toolbar_shadow_dark
- @drawable/dark_selector
- @color/dark_ripple_color
- @drawable/progress_youtube_horizontal_dark
+ - @color/dark_card_item_background_color
+ - @color/dark_card_item_contrast_color
+ - @color/dark_border_color
+ - @drawable/dashed_border_dark
- @style/PreferenceThemeOverlay.v14.Material
@@ -148,6 +228,11 @@
- @color/black_separator_color
- @color/black_contrast_background_color
+
+ - @color/black_card_item_background_color
+ - @color/black_card_item_contrast_color
+ - @color/black_border_color
+ - @drawable/dashed_border_black
@@ -167,6 +252,22 @@
- @color/dark_dialog_background_color
+
+
+
+