diff --git a/app/build.gradle b/app/build.gradle index 434584bf0..b77e6c986 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { resValue "string", "app_name", "NewPipe" minSdkVersion 19 targetSdkVersion 29 - versionCode 950 - versionName "0.19.5" + versionCode 951 + versionName "0.19.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -84,13 +84,18 @@ ext { checkstyleVersion = '8.32' stethoVersion = '1.5.1' leakCanaryVersion = '2.2' - exoPlayerVersion = '2.11.4' + exoPlayerVersion = '2.11.6' androidxLifecycleVersion = '2.2.0' androidxRoomVersion = '2.2.5' groupieVersion = '2.8.0' markwonVersion = '4.3.1' } +configurations { + checkstyle + ktlint +} + checkstyle { configFile rootProject.file('checkstyle.xml') ignoreFailures false @@ -106,8 +111,7 @@ task runCheckstyle(type: Checkstyle) { exclude '**/BuildConfig.java' exclude 'main/java/us/shandian/giga/**' - // empty classpath - classpath = files() + classpath = configurations.checkstyle showViolations true @@ -117,10 +121,6 @@ task runCheckstyle(type: Checkstyle) { } } -configurations { - ktlint -} - task runKtlint(type: JavaExec) { main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint @@ -143,7 +143,7 @@ dependencies { implementation "frankiesardo:icepick:${icepickVersion}" kapt "frankiesardo:icepick-processor:${icepickVersion}" - debugImplementation "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" + checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" ktlint "com.pinterest:ktlint:0.35.0" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" @@ -163,7 +163,7 @@ dependencies { exclude module: 'support-annotations' } - implementation 'com.github.TeamNewPipe:NewPipeExtractor:bda83fe6a5b9a8a0751669fbc444fa49d72d0d2f' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:a70cb0283ffc3bba2709815673a5a7940aab0a3a' implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" implementation "org.jsoup:jsoup:1.13.1" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4764b4c4..c24c91193 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,23 @@ - - - - - - - + + + + + + - + - - - + + + + - + @@ -50,36 +52,36 @@ + android:label="@string/title_activity_background_player" + android:launchMode="singleTask" /> + android:label="@string/title_activity_popup_player" + android:launchMode="singleTask" /> + android:exported="false" /> + android:theme="@style/VideoPlayerTheme" /> + android:label="@string/settings" /> + android:label="@string/title_activity_about" /> - - - + + + - + - + - + android:theme="@android:style/Theme.NoDisplay" /> + + android:launchMode="singleTask" /> - + + android:label="@string/recaptcha" /> + android:resource="@xml/nnf_provider_paths" /> - - - + + + - - + + - - - - - - + + + + + + - - - - + + + + - - - + + + - + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - + + - - - + + + - - + + - - - - + + + + - - - + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - - - - + + + + + + - - - + + + - - - + + + - - + + - - - + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:exported="false" /> diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 3fc3121b1..37d6d62f5 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -34,8 +34,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; @@ -127,12 +125,6 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window w = getWindow(); - w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, - WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); - } - if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) { initFragments(); diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index 2fb8ac7f7..b5be2dde6 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -1,8 +1,6 @@ package org.schabi.newpipe.about; import android.content.Context; -import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.MenuItem; @@ -26,6 +24,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser; public class AboutActivity extends AppCompatActivity { /** @@ -143,28 +142,23 @@ public class AboutActivity extends AppCompatActivity { View githubLink = rootView.findViewById(R.id.github_link); githubLink.setOnClickListener(nv -> - openWebsite(context.getString(R.string.github_url), context)); + openUrlInBrowser(context, context.getString(R.string.github_url))); View donationLink = rootView.findViewById(R.id.donation_link); donationLink.setOnClickListener(v -> - openWebsite(context.getString(R.string.donation_url), context)); + openUrlInBrowser(context, context.getString(R.string.donation_url))); View websiteLink = rootView.findViewById(R.id.website_link); websiteLink.setOnClickListener(nv -> - openWebsite(context.getString(R.string.website_url), context)); + openUrlInBrowser(context, context.getString(R.string.website_url))); View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link); privacyPolicyLink.setOnClickListener(v -> - openWebsite(context.getString(R.string.privacy_policy_url), context)); + openUrlInBrowser(context, context.getString(R.string.privacy_policy_url))); return rootView; } - private void openWebsite(final String url, final Context context) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - context.startActivity(intent); - } - } /** diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index fd99f84a1..3ce95631c 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -1,7 +1,33 @@ package org.schabi.newpipe.database.playlist; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public interface PlaylistLocalItem extends LocalItem { String getOrderingName(); + + static List merge( + final List localPlaylists, + final List remotePlaylists) { + final List items = new ArrayList<>( + localPlaylists.size() + remotePlaylists.size()); + items.addAll(localPlaylists); + items.addAll(remotePlaylists); + + Collections.sort(items, (left, right) -> { + final String on1 = left.getOrderingName(); + final String on2 = right.getOrderingName(); + if (on1 == null) { + return on2 == null ? 0 : 1; + } else { + return on2 == null ? -1 : on1.compareToIgnoreCase(on2); + } + }); + + return items; + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 573fa4b90..60dd343b9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -20,6 +20,45 @@ abstract class SubscriptionDAO : BasicDAO { @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") abstract override fun getAll(): Flowable> + @Query(""" + SELECT * FROM subscriptions + + WHERE name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsFiltered(filter: String): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngrouped( + currentGroupId: Long + ): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + AND s.name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngroupedFiltered( + currentGroupId: Long, + filter: String + ): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> @@ -52,7 +91,7 @@ abstract class SubscriptionDAO : BasicDAO { entity.uid = uidFromInsert } else { val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) - ?: throw IllegalStateException("Subscription cannot be null just after insertion.") + ?: throw IllegalStateException("Subscription cannot be null just after insertion.") entity.uid = subscriptionIdFromDb update(entity) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index cc7219543..a47f17d13 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -130,4 +130,55 @@ public class SubscriptionEntity { item.setDescription(getDescription()); return item; } + + + // TODO: Remove these generated methods by migrating this class to a data class from Kotlin. + @Override + @SuppressWarnings("EqualsReplaceableByObjectsCall") + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final SubscriptionEntity that = (SubscriptionEntity) o; + + if (uid != that.uid) { + return false; + } + if (serviceId != that.serviceId) { + return false; + } + if (!url.equals(that.url)) { + return false; + } + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) { + return false; + } + if (subscriberCount != null + ? !subscriberCount.equals(that.subscriberCount) + : that.subscriberCount != null) { + return false; + } + return description != null + ? description.equals(that.description) + : that.description == null; + } + + @Override + public int hashCode() { + int result = (int) (uid ^ (uid >>> 32)); + result = 31 * result + serviceId; + result = 31 * result + url.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); + result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + return result; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 36e7a206c..fff689930 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -479,7 +479,6 @@ public class VideoDetailFragment extends BaseStateFragment case R.id.detail_controls_download: NavigationHelper.openDownloads(getActivity()); break; - case R.id.detail_uploader_root_layout: if (TextUtils.isEmpty(currentInfo.getSubChannelUrl())) { Log.w(TAG, @@ -488,6 +487,9 @@ public class VideoDetailFragment extends BaseStateFragment openChannel(currentInfo.getUploaderUrl(), currentInfo.getUploaderName()); } break; + case R.id.detail_title_root_layout: + ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString()); + break; } return true; @@ -583,6 +585,7 @@ public class VideoDetailFragment extends BaseStateFragment protected void initListeners() { super.initListeners(); + videoTitleRoot.setOnLongClickListener(this); uploaderRootLayout.setOnClickListener(this); uploaderRootLayout.setOnLongClickListener(this); videoTitleRoot.setOnClickListener(this); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index aed7c4795..82b1d18ed 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.views.NewPipeRecyclerView; @@ -30,7 +31,7 @@ public abstract class BaseListInfoFragment protected String url; protected I currentInfo; - protected String currentNextPageUrl; + protected Page currentNextPage; protected Disposable currentWorker; @Override @@ -78,7 +79,7 @@ public abstract class BaseListInfoFragment public void writeTo(final Queue objectsToSave) { super.writeTo(objectsToSave); objectsToSave.add(currentInfo); - objectsToSave.add(currentNextPageUrl); + objectsToSave.add(currentNextPage); } @Override @@ -86,7 +87,7 @@ public abstract class BaseListInfoFragment public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); currentInfo = (I) savedObjects.poll(); - currentNextPageUrl = (String) savedObjects.poll(); + currentNextPage = (Page) savedObjects.poll(); } /*////////////////////////////////////////////////////////////////////////// @@ -130,7 +131,7 @@ public abstract class BaseListInfoFragment .subscribe((@NonNull I result) -> { isLoading.set(false); currentInfo = result; - currentNextPageUrl = result.getNextPageUrl(); + currentNextPage = result.getNextPage(); handleResult(result); }, (@NonNull Throwable throwable) -> onError(throwable)); } @@ -182,7 +183,7 @@ public abstract class BaseListInfoFragment @Override public void handleNextItems(final ListExtractor.InfoItemsPage result) { super.handleNextItems(result); - currentNextPageUrl = result.getNextPageUrl(); + currentNextPage = result.getNextPage(); infoListAdapter.addInfoItemList(result.getItems()); showListFooter(hasMoreItems()); @@ -190,7 +191,7 @@ public abstract class BaseListInfoFragment @Override protected boolean hasMoreItems() { - return !TextUtils.isEmpty(currentNextPageUrl); + return Page.isValid(currentNextPage); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index b88d375ff..330aa7b42 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -403,7 +403,7 @@ public class ChannelFragment extends BaseListInfoFragment @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage); } @Override @@ -555,7 +555,7 @@ public class ChannelFragment extends BaseListInfoFragment } } return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPageUrl(), streamItems, index); + currentInfo.getNextPage(), streamItems, index); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java index d23293c8a..c8e18f610 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -71,7 +71,7 @@ public class CommentsFragment extends BaseListInfoFragment { @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl); + return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java index 0702553ad..4b758a9c0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/DefaultKioskFragment.java @@ -44,7 +44,7 @@ public class DefaultKioskFragment extends KioskFragment { name = kioskTranslatedName; currentInfo = null; - currentNextPageUrl = null; + currentNextPage = null; } catch (ExtractionException e) { onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java index 21a7944ee..a9dc59951 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/kiosk/KioskFragment.java @@ -150,7 +150,7 @@ public class KioskFragment extends BaseListInfoFragment { @Override public Single loadMoreItemsLogic() { - return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 93df98c97..e2ec9c1f3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -229,7 +229,7 @@ public class PlaylistFragment extends BaseListInfoFragment { @Override protected Single loadMoreItemsLogic() { - return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl); + return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage); } @Override @@ -349,7 +349,7 @@ public class PlaylistFragment extends BaseListInfoFragment { return new PlaylistPlayQueue( currentInfo.getServiceId(), currentInfo.getUrl(), - currentInfo.getNextPageUrl(), + currentInfo.getNextPage(), infoItems, index ); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 5e36d81a2..4f21565f4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -7,6 +7,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; +import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; @@ -37,6 +38,7 @@ import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; @@ -71,6 +73,7 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; +import static android.text.Html.escapeHtml; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; @@ -118,13 +121,18 @@ public class SearchFragment extends BaseListFragment menuItemToFilterName; private StreamingService service; - private String currentPageUrl; - private String nextPageUrl; + private Page nextPage; private String contentCountry; private boolean isSuggestionsEnabled = true; @@ -143,6 +151,8 @@ public class SearchFragment extends BaseListFragment objectsToSave) { super.writeTo(objectsToSave); - objectsToSave.add(currentPageUrl); - objectsToSave.add(nextPageUrl); + objectsToSave.add(nextPage); } @Override public void readFrom(@NonNull final Queue savedObjects) throws Exception { super.readFrom(savedObjects); - currentPageUrl = (String) savedObjects.poll(); - nextPageUrl = (String) savedObjects.poll(); + nextPage = (Page) savedObjects.poll(); } @Override @@ -497,6 +509,8 @@ public class SearchFragment extends BaseListFragment()); showKeyboardSearch(); @@ -554,11 +568,13 @@ public class SearchFragment extends BaseListFragment isLoading.set(false)) @@ -961,9 +973,13 @@ public class SearchFragment extends BaseListFragment" + escapeHtml(searchSuggestion) + ""; + correctSuggestion.setText( + Html.fromHtml(String.format(helperText, highlightedSearchSuggestion))); + + + correctSuggestion.setOnClickListener(v -> { + correctSuggestion.setVisibility(View.GONE); + search(searchSuggestion, contentFilter, sortFilter); + searchEditText.setText(searchSuggestion); + }); + + correctSuggestion.setOnLongClickListener(v -> { + searchEditText.setText(searchSuggestion); + searchEditText.setSelection(searchSuggestion.length()); + showKeyboardSearch(); + return true; + }); + + correctSuggestion.setVisibility(View.VISIBLE); + } + } + @Override public void handleNextItems(final ListExtractor.InfoItemsPage result) { showListFooter(false); - currentPageUrl = result.getNextPageUrl(); infoListAdapter.addInfoItemList(result.getItems()); - nextPageUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), - "\"" + searchString + "\" → page: " + nextPageUrl, 0); + "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " + + "pageIds: " + nextPage.getIds() + ", " + + "pageCookies: " + nextPage.getCookies(), 0); } super.handleNextItems(result); } @@ -1020,6 +1068,10 @@ public class SearchFragment extends BaseListFragment { - if (!AndroidTvUtils.isTv(itemBuilder.getContext())) { - ClipboardManager clipboardManager = (ClipboardManager) itemBuilder.getContext() - .getSystemService(Context.CLIPBOARD_SERVICE); - clipboardManager.setPrimaryClip(ClipData.newPlainText(null, commentText)); - Toast.makeText(itemBuilder.getContext(), R.string.msg_copied, Toast.LENGTH_SHORT) - .show(); - } else { + if (AndroidTvUtils.isTv(itemBuilder.getContext())) { openCommentAuthor(item); + } else { + ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText); } return true; }); diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index d99a05976..7e11d7a2e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -30,8 +30,6 @@ import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import icepick.State; @@ -54,31 +52,6 @@ public final class BookmarkFragment extends BaseLocalListFragment merge( - final List localPlaylists, - final List remotePlaylists) { - List items = new ArrayList<>( - localPlaylists.size() + remotePlaylists.size()); - items.addAll(localPlaylists); - items.addAll(remotePlaylists); - - Collections.sort(items, (left, right) -> { - String on1 = left.getOrderingName(); - String on2 = right.getOrderingName(); - if (on1 == null && on2 == null) { - return 0; - } else if (on1 != null && on2 == null) { - return -1; - } else if (on1 == null && on2 != null) { - return 1; - } else { - return on1.compareToIgnoreCase(on2); - } - }); - - return items; - } - @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -164,7 +137,7 @@ public final class BookmarkFragment extends BaseLocalListFragment> { + return when { + filterQuery.isNotEmpty() -> { + return if (showOnlyUngrouped) { + subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( + currentGroupId, filterQuery) + } else { + subscriptionTable.getSubscriptionsFiltered(filterQuery) + } + } + showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + else -> subscriptionTable.all + } + } + fun upsertAll(infoList: List): List { val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it) }) + infoList.map { SubscriptionEntity.from(it) }) database.runInTransaction { infoList.forEachIndexed { index, info -> @@ -35,13 +56,13 @@ class SubscriptionManager(context: Context) { } 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) - } + .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) @@ -57,8 +78,8 @@ class SubscriptionManager(context: Context) { fun deleteSubscription(serviceId: Int, url: String): Completable { return Completable.fromCallable { subscriptionTable.deleteSubscription(serviceId, url) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { 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 index e9d9ac5b3..66387d298 100644 --- 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 @@ -5,6 +5,7 @@ import android.content.Context import android.os.Bundle import android.os.Parcelable import android.text.Editable +import android.text.TextUtils import android.text.TextWatcher import android.view.LayoutInflater import android.view.View @@ -13,34 +14,22 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.xwray.groupie.GroupAdapter +import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick import icepick.State import java.io.Serializable -import kotlinx.android.synthetic.main.dialog_feed_group_create.cancel_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.confirm_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_screen_message -import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input -import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input_container -import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_preview -import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_selector -import kotlinx.android.synthetic.main.dialog_feed_group_create.options_root -import kotlinx.android.synthetic.main.dialog_feed_group_create.select_channel_button -import kotlinx.android.synthetic.main.dialog_feed_group_create.selected_subscription_count_view -import kotlinx.android.synthetic.main.dialog_feed_group_create.separator -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_header_info -import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_list +import kotlin.collections.contains +import kotlinx.android.synthetic.main.dialog_feed_group_create.* +import kotlinx.android.synthetic.main.toolbar_search_layout.* import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity -import org.schabi.newpipe.database.subscription.SubscriptionEntity +import org.schabi.newpipe.fragments.BackPressable import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.IconPickerScreen @@ -51,9 +40,10 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.Dia 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.AndroidTvUtils import org.schabi.newpipe.util.ThemeHelper -class FeedGroupDialog : DialogFragment() { +class FeedGroupDialog : DialogFragment(), BackPressable { private lateinit var viewModel: FeedGroupDialogViewModel private var groupId: Long = NO_GROUP_SELECTED private var groupIcon: FeedGroupIcon? = null @@ -66,22 +56,20 @@ class FeedGroupDialog : DialogFragment() { object DeleteScreen : ScreenState() } - @State - @JvmField - var selectedIcon: FeedGroupIcon? = null - @State - @JvmField - var selectedSubscriptions: HashSet = HashSet() - @State - @JvmField - var currentScreen: ScreenState = InitialScreen + @State @JvmField var selectedIcon: FeedGroupIcon? = null + @State @JvmField var selectedSubscriptions: HashSet = HashSet() + @State @JvmField var wasSubscriptionSelectionChanged: Boolean = false + @State @JvmField var currentScreen: ScreenState = InitialScreen - @State - @JvmField - var subscriptionsListState: Parcelable? = null - @State - @JvmField - var iconsListState: Parcelable? = null + @State @JvmField var subscriptionsListState: Parcelable? = null + @State @JvmField var iconsListState: Parcelable? = null + @State @JvmField var wasSearchSubscriptionsVisible = false + @State @JvmField var subscriptionsCurrentSearchQuery = "" + @State @JvmField var subscriptionsShowOnlyUngrouped = false + + private val subscriptionMainSection = Section() + private val subscriptionEmptyFooter = Section() + private lateinit var subscriptionGroupAdapter: GroupAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,22 +79,30 @@ class FeedGroupDialog : DialogFragment() { groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + 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 { + if (!this@FeedGroupDialog.onBackPressed()) { super.onBackPressed() } } } } + override fun onPause() { + super.onPause() + + wasSearchSubscriptionsVisible = isSearchVisible() + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) @@ -119,11 +115,15 @@ class FeedGroupDialog : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProviders.of(this, FeedGroupDialogViewModel.Factory(requireContext(), groupId)) - .get(FeedGroupDialogViewModel::class.java) + viewModel = ViewModelProvider(this, + FeedGroupDialogViewModel.Factory(requireContext(), + groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped) + ).get(FeedGroupDialogViewModel::class.java) viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) - viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { setupSubscriptionPicker(it.first, it.second) }) + viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { + setupSubscriptionPicker(it.first, it.second) + }) viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { when (it) { ProcessingEvent -> disableInput() @@ -131,15 +131,54 @@ class FeedGroupDialog : DialogFragment() { } }) + subscriptionGroupAdapter = GroupAdapter().apply { + add(subscriptionMainSection) + add(subscriptionEmptyFooter) + spanCount = 4 + } + subscriptions_selector_list.apply { + // Disable animations, too distracting. + itemAnimator = null + adapter = subscriptionGroupAdapter + layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount, + RecyclerView.VERTICAL, false).apply { + spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup + } + } + setupIconPicker() setupListeners() showScreen(currentScreen) + + if (currentScreen == SubscriptionsPickerScreen && wasSearchSubscriptionsVisible) { + showSearch() + } else if (currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED) { + showKeyboard() + } } - // ///////////////////////////////////////////////////////////////////////// + override fun onDestroyView() { + super.onDestroyView() + subscriptions_selector_list?.adapter = null + icon_selector?.adapter = null + } + + /*/​////////////////////////////////////////////////////////////////////////// // Setup - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ + + override fun onBackPressed(): Boolean { + if (currentScreen is SubscriptionsPickerScreen && isSearchVisible()) { + hideSearch() + return true + } else if (currentScreen !is InitialScreen) { + showScreen(InitialScreen) + return true + } + + return false + } private fun setupListeners() { delete_button.setOnClickListener { showScreen(DeleteScreen) } @@ -163,13 +202,64 @@ class FeedGroupDialog : DialogFragment() { } }) - confirm_button.setOnClickListener { - when (currentScreen) { - InitialScreen -> handlePositiveButtonInitialScreen() - DeleteScreen -> viewModel.deleteGroup() - else -> showScreen(InitialScreen) + confirm_button.setOnClickListener { handlePositiveButton() } + + select_channel_button.setOnClickListener { + subscriptions_selector_list.scrollToPosition(0) + showScreen(SubscriptionsPickerScreen) + } + + val headerMenu = subscriptions_header_toolbar.menu + requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu) + + headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener { + showSearch() + true + } + + headerMenu.findItem(R.id.feed_group_toggle_show_only_ungrouped_subscriptions).apply { + isChecked = subscriptionsShowOnlyUngrouped + setOnMenuItemClickListener { + subscriptionsShowOnlyUngrouped = !subscriptionsShowOnlyUngrouped + it.isChecked = subscriptionsShowOnlyUngrouped + viewModel.toggleShowOnlyUngrouped(subscriptionsShowOnlyUngrouped) + true } } + + toolbar_search_clear.setOnClickListener { + if (TextUtils.isEmpty(toolbar_search_edit_text.text)) { + hideSearch() + return@setOnClickListener + } + resetSearch() + showKeyboardSearch() + } + + toolbar_search_edit_text.setOnClickListener { + if (AndroidTvUtils.isTv(context)) { + showKeyboardSearch() + } + } + + toolbar_search_edit_text.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun afterTextChanged(s: Editable) = Unit + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + val newQuery: String = toolbar_search_edit_text.text.toString() + subscriptionsCurrentSearchQuery = newQuery + viewModel.filterSubscriptionsBy(newQuery) + } + }) + + subscriptionGroupAdapter?.setOnItemClickListener(subscriptionPickerItemListener) + } + + private fun handlePositiveButton() = when { + currentScreen is InitialScreen -> handlePositiveButtonInitialScreen() + currentScreen is DeleteScreen -> viewModel.deleteGroup() + currentScreen is SubscriptionsPickerScreen && isSearchVisible() -> hideSearch() + else -> showScreen(InitialScreen) } private fun handlePositiveButtonInitialScreen() { @@ -202,80 +292,73 @@ class FeedGroupDialog : DialogFragment() { groupIcon = feedGroupEntity?.icon groupSortOrder = feedGroupEntity?.sortOrder ?: -1 - icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext())) + val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!! + icon_preview.setImageResource(feedGroupIcon.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() + private val subscriptionPickerItemListener = OnItemClickListener { item, view -> + if (item is PickerSubscriptionItem) { + val subscriptionId = item.subscriptionEntity.uid + wasSubscriptionSelectionChanged = true - val groupAdapter = GroupAdapter() - groupAdapter.spanCount = if (useGridLayout) 4 else 1 - - val subscriptionsCount = this.selectedSubscriptions.size - val selectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount) - 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) + val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { + this.selectedSubscriptions.remove(subscriptionId) + false } else { - LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) + this.selectedSubscriptions.add(subscriptionId) + true } - adapter = groupAdapter + item.updateSelected(view, isSelected) + updateSubscriptionSelectedCount() + } + } - if (subscriptionsListState != null) { - layoutManager?.onRestoreInstanceState(subscriptionsListState) - subscriptionsListState = null - } + private fun setupSubscriptionPicker( + subscriptions: List, + selectedSubscriptions: Set + ) { + if (!wasSubscriptionSelectionChanged) { + this.selectedSubscriptions.addAll(selectedSubscriptions) } - groupAdapter.setOnItemClickListener { item, _ -> - when (item) { - is PickerSubscriptionItem -> { - val subscriptionId = item.subscriptionEntity.uid + updateSubscriptionSelectedCount() - 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 subscriptionsCount = this.selectedSubscriptions.size - val updateSelectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount) - selected_subscription_count_view.text = updateSelectedCountText - subscriptions_selector_header_info.text = updateSelectedCountText - } - } + if (subscriptions.isEmpty()) { + subscriptionEmptyFooter.clear() + subscriptionEmptyFooter.add(EmptyPlaceholderItem()) + } else { + subscriptionEmptyFooter.clear() } - select_channel_button.setOnClickListener { + subscriptions.forEach { + it.isSelected = this@FeedGroupDialog.selectedSubscriptions + .contains(it.subscriptionEntity.uid) + } + + subscriptionMainSection.update(subscriptions, false) + + if (subscriptionsListState != null) { + subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState) + subscriptionsListState = null + } else { subscriptions_selector_list.scrollToPosition(0) - showScreen(SubscriptionsPickerScreen) } } + private fun updateSubscriptionSelectedCount() { + val selectedCount = this.selectedSubscriptions.size + val selectedCountText = resources.getQuantityString( + R.plurals.feed_group_dialog_selection_count, + selectedCount, selectedCount) + selected_subscription_count_view.text = selectedCountText + subscriptions_header_info.text = selectedCountText + } + private fun setupIconPicker() { val groupAdapter = GroupAdapter() groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) @@ -311,9 +394,9 @@ class FeedGroupDialog : DialogFragment() { } } - // ///////////////////////////////////////////////////////////////////////// + /*/​////////////////////////////////////////////////////////////////////////// // Screen Selector - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ private fun showScreen(screen: ScreenState) { currentScreen = screen @@ -337,7 +420,8 @@ class FeedGroupDialog : DialogFragment() { else -> View.VISIBLE } - if (currentScreen != InitialScreen) hideKeyboard() + hideKeyboard() + hideSearch() } private fun View.onlyVisibleIn(vararg screens: ScreenState) { @@ -347,13 +431,58 @@ class FeedGroupDialog : DialogFragment() { } } - // ///////////////////////////////////////////////////////////////////////// + /*/​////////////////////////////////////////////////////////////////////////// // Utils - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ + + private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE + + private fun resetSearch() { + toolbar_search_edit_text.setText("") + subscriptionsCurrentSearchQuery = "" + viewModel.clearSubscriptionsFilter() + } + + private fun hideSearch() { + resetSearch() + subscriptions_header_search_container.visibility = View.GONE + subscriptions_header_info_container.visibility = View.VISIBLE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true + hideKeyboardSearch() + } + + private fun showSearch() { + subscriptions_header_search_container.visibility = View.VISIBLE + subscriptions_header_info_container.visibility = View.GONE + subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false + showKeyboardSearch() + } + + private val inputMethodManager by lazy { + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + } + + private fun showKeyboardSearch() { + if (toolbar_search_edit_text.requestFocus()) { + inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT) + } + } + + private fun hideKeyboardSearch() { + inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) + toolbar_search_edit_text.clearFocus() + } + + private fun showKeyboard() { + if (group_name_input.requestFocus()) { + inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT) + } + } private fun hideKeyboard() { - val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN) + inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN) group_name_input.clearFocus() } 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 index ac00245e6..e9a7e4eb7 100644 --- 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 @@ -9,42 +9,56 @@ import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.disposables.Disposable import io.reactivex.functions.BiFunction +import io.reactivex.processors.BehaviorProcessor 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 +import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem -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 - } - } +class FeedGroupDialogViewModel( + applicationContext: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + initialQuery: String = "", + initialShowOnlyUngrouped: Boolean = false +) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private var subscriptionManager = SubscriptionManager(applicationContext) + private var filterSubscriptions = BehaviorProcessor.create() + private var toggleShowOnlyUngrouped = BehaviorProcessor.create() + + private var subscriptionsFlowable = Flowable + .combineLatest( + filterSubscriptions.startWith(initialQuery), + toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), + BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } + ) + .distinctUntilChanged() + .switchMap { filter -> + subscriptionManager.getSubscriptions(groupId, filter.query, filter.showOnlyUngrouped) + }.map { list -> list.map { PickerSubscriptionItem(it) } } + private val mutableGroupLiveData = MutableLiveData() - private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() + private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() private val mutableDialogEventLiveData = MutableLiveData() val groupLiveData: LiveData = mutableGroupLiveData - val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData + 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) + .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) + .combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), + BiFunction { t1: List, t2: List -> t1 to t2.toSet() }) + .subscribeOn(Schedulers.io()) + .subscribe(mutableSubscriptionsLiveData::postValue) override fun onCleared() { super.onCleared() @@ -55,14 +69,14 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set) { doAction(feedDatabaseManager.createGroup(name, selectedIcon) - .flatMapCompletable { - feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) - }) + .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)))) + .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) } fun deleteGroup() { @@ -74,13 +88,40 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent actionProcessingDisposable = completable - .subscribeOn(Schedulers.io()) - .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } + .subscribeOn(Schedulers.io()) + .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } } } + fun filterSubscriptionsBy(query: String) { + filterSubscriptions.onNext(query) + } + + fun clearSubscriptionsFilter() { + filterSubscriptions.onNext("") + } + + fun toggleShowOnlyUngrouped(showOnlyUngrouped: Boolean) { + toggleShowOnlyUngrouped.onNext(showOnlyUngrouped) + } + sealed class DialogEvent { object ProcessingEvent : DialogEvent() object SuccessEvent : DialogEvent() } + + data class Filter(val query: String, val showOnlyUngrouped: Boolean) + + class Factory( + private val context: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + private val initialQuery: String = "", + private val initialShowOnlyUngrouped: Boolean = false + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedGroupDialogViewModel(context.applicationContext, + groupId, initialQuery, initialShowOnlyUngrouped) as T + } + } } 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 index c806277ee..ef7eb93cd 100644 --- 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 @@ -7,4 +7,5 @@ import org.schabi.newpipe.R class EmptyPlaceholderItem : Item() { override fun getLayout(): Int = R.layout.list_empty_view override fun bind(viewHolder: GroupieViewHolder, position: Int) {} + override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount } 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 index d90ac0d82..7d33da71f 100644 --- 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 @@ -1,39 +1,28 @@ 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.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.Item -import kotlinx.android.synthetic.main.picker_subscription_item.selected_highlight -import kotlinx.android.synthetic.main.picker_subscription_item.thumbnail_view -import kotlinx.android.synthetic.main.picker_subscription_item.title_view +import kotlinx.android.synthetic.main.picker_subscription_item.* +import kotlinx.android.synthetic.main.picker_subscription_item.view.* 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 - } - +data class PickerSubscriptionItem( + val subscriptionEntity: SubscriptionEntity, + var isSelected: Boolean = false +) : Item() { + override fun getId(): Long = subscriptionEntity.uid 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 getSpanSize(spanCount: Int, position: Int): Int = 1 override fun bind(viewHolder: GroupieViewHolder, position: Int) { - ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, viewHolder.thumbnail_view, IMAGE_LOADING_OPTIONS) + ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, + viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS) viewHolder.title_view.text = subscriptionEntity.name viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE @@ -47,7 +36,9 @@ data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, va viewHolder.selected_highlight.alpha = 1F } - override fun getId(): Long { - return subscriptionEntity.uid + fun updateSelected(containerView: View, isSelected: Boolean) { + this.isSelected = isSelected + animateView(containerView.selected_highlight, + AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) } } diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index f5a669788..61c5d9e68 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -64,7 +64,6 @@ import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.MediaSessionManager; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerHelper; -import org.schabi.newpipe.player.mediasource.FailedMediaSource; import org.schabi.newpipe.player.playback.BasePlayerMediaSession; import org.schabi.newpipe.player.playback.CustomTrackSelector; import org.schabi.newpipe.player.playback.MediaSourceManager; @@ -77,7 +76,6 @@ import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.SerializedCache; import java.io.IOException; -import java.net.UnknownHostException; import io.reactivex.Observable; import io.reactivex.disposables.CompositeDisposable; @@ -217,7 +215,7 @@ public abstract class BasePlayer implements final TrackSelection.Factory trackSelectionFactory = PlayerHelper .getQualitySelector(context); - this.trackSelector = new CustomTrackSelector(trackSelectionFactory); + this.trackSelector = new CustomTrackSelector(context, trackSelectionFactory); this.loadControl = new LoadController(); this.renderFactory = new DefaultRenderersFactory(context); @@ -333,13 +331,12 @@ public abstract class BasePlayer implements final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - final float speed = preferences - .getFloat(context.getString(R.string.playback_speed_key), getPlaybackSpeed()); - final float pitch = preferences.getFloat(context.getString(R.string.playback_pitch_key), - getPlaybackPitch()); - final boolean skipSilence = preferences - .getBoolean(context.getString(R.string.playback_skip_silence_key), - getPlaybackSkipSilence()); + final float speed = preferences.getFloat( + context.getString(R.string.playback_speed_key), getPlaybackSpeed()); + final float pitch = preferences.getFloat( + context.getString(R.string.playback_pitch_key), getPlaybackPitch()); + final boolean skipSilence = preferences.getBoolean( + context.getString(R.string.playback_skip_silence_key), getPlaybackSkipSilence()); return new PlaybackParameters(speed, pitch, skipSilence); } @@ -835,16 +832,8 @@ public abstract class BasePlayer implements final Throwable cause = error.getCause(); if (error instanceof BehindLiveWindowException) { reload(); - } else if (cause instanceof UnknownHostException) { - playQueue.error(/*isNetworkProblem=*/true); - } else if (isCurrentWindowValid()) { - playQueue.error(/*isTransitioningToBadStream=*/true); - } else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) { - playQueue.error(/*recoverableWithNoAvailableStream=*/false); - } else if (cause instanceof FailedMediaSource.StreamInfoLoadException) { - playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false); } else { - playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true); + playQueue.error(); } } @@ -1131,6 +1120,7 @@ public abstract class BasePlayer implements Log.d(TAG, "onFastRewind() called"); } seekBy(-getSeekDuration()); + triggerProgressUpdate(); } public void onFastForward() { @@ -1138,6 +1128,7 @@ public abstract class BasePlayer implements Log.d(TAG, "onFastForward() called"); } seekBy(getSeekDuration()); + triggerProgressUpdate(); } private int getSeekDuration() { @@ -1479,10 +1470,21 @@ public abstract class BasePlayer implements return parameters == null ? PlaybackParameters.DEFAULT : parameters; } + /** + * Sets the playback parameters of the player, and also saves them to shared preferences. + * Speed and pitch are rounded up to 2 decimal places before being used or saved. + * @param speed the playback speed, will be rounded to up to 2 decimal places + * @param pitch the playback pitch, will be rounded to up to 2 decimal places + * @param skipSilence skip silence during playback + */ public void setPlaybackParameters(final float speed, final float pitch, final boolean skipSilence) { - savePlaybackParametersToPreferences(speed, pitch, skipSilence); - simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence)); + final float roundedSpeed = Math.round(speed * 100.0f) / 100.0f; + final float roundedPitch = Math.round(pitch * 100.0f) / 100.0f; + + savePlaybackParametersToPreferences(roundedSpeed, roundedPitch, skipSilence); + simpleExoPlayer.setPlaybackParameters( + new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence)); } private void savePlaybackParametersToPreferences(final float speed, final float pitch, diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 7a5faf31d..0ccec3067 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -166,9 +166,6 @@ public final class PopupVideoPlayer extends Service { initPopup(); initPopupCloseOverlay(); } - if (!playerImpl.isPlaying()) { - playerImpl.getPlayer().setPlayWhenReady(true); - } playerImpl.handleIntent(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 2c9c4a425..72becef8f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -45,7 +45,6 @@ import org.schabi.newpipe.util.ThemeHelper; import java.util.Collections; import java.util.List; -import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; @@ -84,14 +83,13 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private ImageButton repeatButton; private ImageButton backwardButton; + private ImageButton fastRewindButton; private ImageButton playPauseButton; + private ImageButton fastForwardButton; private ImageButton forwardButton; private ImageButton shuffleButton; private ProgressBar progressBar; - private TextView playbackSpeedButton; - private TextView playbackPitchButton; - private Menu menu; //////////////////////////////////////////////////////////////////////////// @@ -166,6 +164,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity case R.id.action_append_playlist: appendAllToPlaylist(); return true; + case R.id.action_playback_speed: + openPlaybackParameterDialog(); + return true; case R.id.action_mute: player.onMuteUnmuteButtonClicked(); return true; @@ -310,20 +311,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void buildControls() { repeatButton = rootView.findViewById(R.id.control_repeat); backwardButton = rootView.findViewById(R.id.control_backward); + fastRewindButton = rootView.findViewById(R.id.control_fast_rewind); playPauseButton = rootView.findViewById(R.id.control_play_pause); + fastForwardButton = rootView.findViewById(R.id.control_fast_forward); forwardButton = rootView.findViewById(R.id.control_forward); shuffleButton = rootView.findViewById(R.id.control_shuffle); - playbackSpeedButton = rootView.findViewById(R.id.control_playback_speed); - playbackPitchButton = rootView.findViewById(R.id.control_playback_pitch); progressBar = rootView.findViewById(R.id.control_progress_bar); repeatButton.setOnClickListener(this); backwardButton.setOnClickListener(this); + fastRewindButton.setOnClickListener(this); playPauseButton.setOnClickListener(this); + fastForwardButton.setOnClickListener(this); forwardButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); - playbackSpeedButton.setOnClickListener(this); - playbackPitchButton.setOnClickListener(this); } private void buildItemPopupMenu(final PlayQueueItem item, final View view) { @@ -473,16 +474,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity player.onRepeatClicked(); } else if (view.getId() == backwardButton.getId()) { player.onPlayPrevious(); + } else if (view.getId() == fastRewindButton.getId()) { + player.onFastRewind(); } else if (view.getId() == playPauseButton.getId()) { player.onPlayPause(); + } else if (view.getId() == fastForwardButton.getId()) { + player.onFastForward(); } else if (view.getId() == forwardButton.getId()) { player.onPlayNext(); } else if (view.getId() == shuffleButton.getId()) { player.onShuffleClicked(); - } else if (view.getId() == playbackSpeedButton.getId()) { - openPlaybackParameterDialog(); - } else if (view.getId() == playbackPitchButton.getId()) { - openPlaybackParameterDialog(); } else if (view.getId() == metadata.getId()) { scrollToSelected(); } else if (view.getId() == progressLiveSync.getId()) { @@ -690,8 +691,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity private void onPlaybackParameterChanged(final PlaybackParameters parameters) { if (parameters != null) { - playbackSpeedButton.setText(formatSpeed(parameters.speed)); - playbackPitchButton.setText(formatPitch(parameters.pitch)); + if (menu != null && player != null) { + final MenuItem item = menu.findItem(R.id.action_playback_speed); + item.setTitle(formatSpeed(parameters.speed)); + } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java index 2ba05b443..e554059d9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.playback; +import android.content.Context; import android.text.TextUtils; import android.util.Pair; @@ -26,8 +27,9 @@ import com.google.android.exoplayer2.util.Assertions; public class CustomTrackSelector extends DefaultTrackSelector { private String preferredTextLanguage; - public CustomTrackSelector(final TrackSelection.Factory adaptiveTrackSelectionFactory) { - super(adaptiveTrackSelectionFactory); + public CustomTrackSelector(final Context context, + final TrackSelection.Factory adaptiveTrackSelectionFactory) { + super(context, adaptiveTrackSelectionFactory); } private static boolean formatHasLanguage(final Format format, final String language) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java index f0d6dc6ec..cde376f4f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java @@ -5,6 +5,7 @@ import android.util.Log; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import java.util.ArrayList; @@ -21,7 +22,7 @@ abstract class AbstractInfoPlayQueue ext final int serviceId; final String baseUrl; - String nextUrl; + Page nextPage; private transient Disposable fetchReactor; @@ -29,16 +30,16 @@ abstract class AbstractInfoPlayQueue ext this(item.getServiceId(), item.getUrl(), null, Collections.emptyList(), 0); } - AbstractInfoPlayQueue(final int serviceId, final String url, final String nextPageUrl, + AbstractInfoPlayQueue(final int serviceId, final String url, final Page nextPage, final List streams, final int index) { super(index, extractListItems(streams)); this.baseUrl = url; - this.nextUrl = nextPageUrl; + this.nextPage = nextPage; this.serviceId = serviceId; this.isInitial = streams.isEmpty(); - this.isComplete = !isInitial && (nextPageUrl == null || nextPageUrl.isEmpty()); + this.isComplete = !isInitial && !Page.isValid(nextPage); } protected abstract String getTag(); @@ -66,7 +67,7 @@ abstract class AbstractInfoPlayQueue ext if (!result.hasNextPage()) { isComplete = true; } - nextUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); append(extractListItems(result.getRelatedItems())); @@ -100,7 +101,7 @@ abstract class AbstractInfoPlayQueue ext if (!result.hasNextPage()) { isComplete = true; } - nextUrl = result.getNextPageUrl(); + nextPage = result.getNextPage(); append(extractListItems(result.getItems())); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java index 5a2e34d31..9e0d2b694 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/ChannelPlayQueue.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.playqueue; +import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -17,15 +18,15 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -41,7 +42,7 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue * This is done as a separate event as the underlying manager may have * different implementation regarding exceptions. *

- * - * @param skippable whether the error could be skipped */ - public synchronized void error(final boolean skippable) { - final int index = getIndex(); - - if (skippable) { - queueIndex.incrementAndGet(); - } else { - removeInternal(index); - } - - broadcast(new ErrorEvent(index, getIndex(), skippable)); + public synchronized void error() { + final int oldIndex = getIndex(); + queueIndex.incrementAndGet(); + broadcast(new ErrorEvent(oldIndex, getIndex())); } private synchronized void removeInternal(final int removeIndex) { diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java index bf1361fc5..f8777597a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java @@ -115,9 +115,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter streams, final int index) { - super(serviceId, url, nextPageUrl, streams, index); + super(serviceId, url, nextPage, streams, index); } @Override @@ -40,7 +41,7 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue { - Context context = this; - new AlertDialog.Builder(context) - .setIcon(android.R.drawable.ic_dialog_alert) - .setTitle(R.string.privacy_policy_title) - .setMessage(R.string.start_accept_privacy_policy) - .setCancelable(false) - .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { - Intent webIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(context.getString(R.string.privacy_policy_url)) - ); - context.startActivity(webIntent); - }) - .setPositiveButton(R.string.accept, (dialog, which) -> { - final Intent i = new Intent(Intent.ACTION_SENDTO) - .setData(Uri.parse("mailto:")) // only email apps should handle this - .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) - .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) - .putExtra(Intent.EXTRA_TEXT, buildJson()); - if (i.resolveActivity(getPackageManager()) != null) { - startActivity(i); - } - - }) - .setNegativeButton(R.string.decline, (dialog, which) -> { - // do nothing - }) - .show(); - + reportEmailButton.setOnClickListener((View v) -> { + openPrivacyPolicyDialog(this, "EMAIL"); }); + copyButton.setOnClickListener((View v) -> { + ShareUtils.copyToClipboard(this, buildMarkdown()); + Toast.makeText(this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); + }); + + reportGithubButton.setOnClickListener((View v) -> { + openPrivacyPolicyDialog(this, "GITHUB"); + }); + + // normal bugreport buildInfo(errorInfo); if (errorInfo.message != 0) { @@ -250,7 +241,7 @@ public class ErrorActivity extends AppCompatActivity { errorView.setText(formErrorText(errorList)); - //print stack trace once again for debugging: + // print stack trace once again for debugging: for (String e : errorList) { Log.e(TAG, e); } @@ -281,6 +272,37 @@ public class ErrorActivity extends AppCompatActivity { return false; } + private void openPrivacyPolicyDialog(final Context context, final String action) { + new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.privacy_policy_title) + .setMessage(R.string.start_accept_privacy_policy) + .setCancelable(false) + .setNeutralButton(R.string.read_privacy_policy, (dialog, which) -> { + ShareUtils.openUrlInBrowser(context, + context.getString(R.string.privacy_policy_url)); + }) + .setPositiveButton(R.string.accept, (dialog, which) -> { + if (action.equals("EMAIL")) { // send on email + final Intent i = new Intent(Intent.ACTION_SENDTO) + .setData(Uri.parse("mailto:")) // only email apps should handle this + .putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS}) + .putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT) + .putExtra(Intent.EXTRA_TEXT, buildJson()); + if (i.resolveActivity(getPackageManager()) != null) { + startActivity(i); + } + } else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub + ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL); + } + + }) + .setNegativeButton(R.string.decline, (dialog, which) -> { + // do nothing + }) + .show(); + } + private String formErrorText(final String[] el) { StringBuilder text = new StringBuilder(); if (el != null) { @@ -331,7 +353,9 @@ public class ErrorActivity extends AppCompatActivity { text += getUserActionString(info.userAction) + "\n" + info.request + "\n" - + getContentLangString() + "\n" + + getContentLanguageString() + "\n" + + getContentCountryString() + "\n" + + getAppLanguage() + "\n" + info.serviceName + "\n" + currentTimeStamp + "\n" + getPackageName() + "\n" @@ -347,7 +371,9 @@ public class ErrorActivity extends AppCompatActivity { .object() .value("user_action", getUserActionString(errorInfo.userAction)) .value("request", errorInfo.request) - .value("content_language", getContentLangString()) + .value("content_language", getContentLanguageString()) + .value("content_country", getContentCountryString()) + .value("app_language", getAppLanguage()) .value("service", errorInfo.serviceName) .value("package", getPackageName()) .value("version", BuildConfig.VERSION_NAME) @@ -365,6 +391,63 @@ public class ErrorActivity extends AppCompatActivity { return ""; } + private String buildMarkdown() { + try { + final StringBuilder htmlErrorReport = new StringBuilder(); + + final String userComment = userCommentBox.getText().toString(); + if (!userComment.isEmpty()) { + htmlErrorReport.append(userComment).append("\n"); + } + + // basic error info + htmlErrorReport + .append("## Exception") + .append("\n* __User Action:__ ") + .append(getUserActionString(errorInfo.userAction)) + .append("\n* __Request:__ ").append(errorInfo.request) + .append("\n* __Content Country:__ ").append(getContentCountryString()) + .append("\n* __Content Language:__ ").append(getContentLanguageString()) + .append("\n* __App Language:__ ").append(getAppLanguage()) + .append("\n* __Service:__ ").append(errorInfo.serviceName) + .append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME) + .append("\n* __OS:__ ").append(getOsString()).append("\n"); + + + // Collapse all logs to a single paragraph when there are more than one + // to keep the GitHub issue clean. + if (errorList.length > 1) { + htmlErrorReport + .append("
Exceptions (") + .append(errorList.length) + .append(")

\n"); + } + + // add the logs + for (int i = 0; i < errorList.length; i++) { + htmlErrorReport.append("

Crash log "); + if (errorList.length > 1) { + htmlErrorReport.append(i + 1); + } + htmlErrorReport.append("") + .append("

\n") + .append("\n```\n").append(errorList[i]).append("\n```\n") + .append("

\n"); + } + + // make sure to close everything + if (errorList.length > 1) { + htmlErrorReport.append("

\n"); + } + htmlErrorReport.append("
\n"); + return htmlErrorReport.toString(); + } catch (Throwable e) { + Log.e(TAG, "Error while erroring: Could not build markdown"); + e.printStackTrace(); + return ""; + } + } + private String getUserActionString(final UserAction userAction) { if (userAction == null) { return "Your description is in another castle."; @@ -373,24 +456,27 @@ public class ErrorActivity extends AppCompatActivity { } } - private String getContentLangString() { - String contentLanguage = PreferenceManager.getDefaultSharedPreferences(this) - .getString(this.getString(R.string.content_country_key), "none"); - if (contentLanguage.equals(getString(R.string.default_localization_key))) { - contentLanguage = Locale.getDefault().toString(); - } - return contentLanguage; + private String getContentCountryString() { + return Localization.getPreferredContentCountry(this).getCountryCode(); + } + + private String getContentLanguageString() { + return Localization.getPreferredLocalization(this).getLocalizationCode(); + } + + private String getAppLanguage() { + return Localization.getAppLocale(getApplicationContext()).toString(); } private String getOsString() { - String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; + final String osBase = Build.VERSION.SDK_INT >= 23 ? Build.VERSION.BASE_OS : "Android"; return System.getProperty("os.name") + " " + (osBase.isEmpty() ? "Android" : osBase) + " " + Build.VERSION.RELEASE + " - " + Build.VERSION.SDK_INT; } - private void addGuruMeditaion() { + private void addGuruMeditation() { //just an easter egg TextView sorryView = findViewById(R.id.errorSorryView); String text = sorryView.getText().toString(); 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 5b452430b..df529fee0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -64,7 +64,7 @@ public class SelectChannelFragment extends DialogFragment { private final ImageLoader imageLoader = ImageLoader.getInstance(); - private OnSelectedLisener onSelectedLisener = null; + private OnSelectedListener onSelectedListener = null; private OnCancelListener onCancelListener = null; private ProgressBar progressBar; @@ -73,8 +73,8 @@ public class SelectChannelFragment extends DialogFragment { private List subscriptions = new Vector<>(); - public void setOnSelectedLisener(final OnSelectedLisener listener) { - onSelectedLisener = listener; + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } public void setOnCancelListener(final OnCancelListener listener) { @@ -129,9 +129,9 @@ public class SelectChannelFragment extends DialogFragment { } private void clickedItem(final int position) { - if (onSelectedLisener != null) { + if (onSelectedListener != null) { SubscriptionEntity entry = subscriptions.get(position); - onSelectedLisener + onSelectedListener .onChannelSelected(entry.getServiceId(), entry.getUrl(), entry.getName()); } dismiss(); @@ -186,7 +186,7 @@ public class SelectChannelFragment extends DialogFragment { // Interfaces //////////////////////////////////////////////////////////////////////////*/ - public interface OnSelectedLisener { + public interface OnSelectedListener { void onChannelSelected(int serviceId, String url, String name); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java index 4df70ccec..13d34dec8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectKioskFragment.java @@ -52,11 +52,11 @@ public class SelectKioskFragment extends DialogFragment { private RecyclerView recyclerView = null; private SelectKioskAdapter selectKioskAdapter = null; - private OnSelectedLisener onSelectedLisener = null; + private OnSelectedListener onSelectedListener = null; private OnCancelListener onCancelListener = null; - public void setOnSelectedLisener(final OnSelectedLisener listener) { - onSelectedLisener = listener; + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; } public void setOnCancelListener(final OnCancelListener listener) { @@ -102,8 +102,8 @@ public class SelectKioskFragment extends DialogFragment { } private void clickedItem(final SelectKioskAdapter.Entry entry) { - if (onSelectedLisener != null) { - onSelectedLisener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); + if (onSelectedListener != null) { + onSelectedListener.onKioskSelected(entry.serviceId, entry.kioskId, entry.kioskName); } dismiss(); } @@ -122,7 +122,7 @@ public class SelectKioskFragment extends DialogFragment { // Interfaces //////////////////////////////////////////////////////////////////////////*/ - public interface OnSelectedLisener { + public interface OnSelectedListener { void onKioskSelected(int serviceId, String kioskId, String kioskName); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java new file mode 100644 index 000000000..1d5c94421 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -0,0 +1,225 @@ +package org.schabi.newpipe.settings; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; + +import java.util.List; +import java.util.Vector; + +import io.reactivex.Flowable; +import io.reactivex.disposables.Disposable; + +public class SelectPlaylistFragment extends DialogFragment { + /** + * This contains the base display options for images. + */ + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS + = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + + private final ImageLoader imageLoader = ImageLoader.getInstance(); + + private OnSelectedListener onSelectedListener = null; + private OnCancelListener onCancelListener = null; + + private ProgressBar progressBar; + private TextView emptyView; + private RecyclerView recyclerView; + private Disposable playlistsSubscriber; + + private List playlists = new Vector<>(); + + public void setOnSelectedListener(final OnSelectedListener listener) { + onSelectedListener = listener; + } + + public void setOnCancelListener(final OnCancelListener listener) { + onCancelListener = listener; + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's Lifecycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + final Bundle savedInstanceState) { + final View v = + inflater.inflate(R.layout.select_playlist_fragment, container, false); + recyclerView = v.findViewById(R.id.items_list); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + SelectPlaylistAdapter playlistAdapter = new SelectPlaylistAdapter(); + recyclerView.setAdapter(playlistAdapter); + + progressBar = v.findViewById(R.id.progressBar); + emptyView = v.findViewById(R.id.empty_state_view); + progressBar.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + emptyView.setVisibility(View.GONE); + + final AppDatabase database = NewPipeDatabase.getInstance(requireContext()); + final LocalPlaylistManager localPlaylistManager = new LocalPlaylistManager(database); + final RemotePlaylistManager remotePlaylistManager = new RemotePlaylistManager(database); + + playlistsSubscriber = Flowable.combineLatest(localPlaylistManager.getPlaylists(), + remotePlaylistManager.getPlaylists(), PlaylistLocalItem::merge) + .subscribe(this::displayPlaylists, this::onError); + + return v; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (playlistsSubscriber != null) { + playlistsSubscriber.dispose(); + playlistsSubscriber = null; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Handle actions + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onCancel(final DialogInterface dialogInterface) { + super.onCancel(dialogInterface); + if (onCancelListener != null) { + onCancelListener.onCancel(); + } + } + + private void clickedItem(final int position) { + if (onSelectedListener != null) { + final LocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + onSelectedListener + .onLocalPlaylistSelected(entry.uid, entry.name); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + onSelectedListener.onRemotePlaylistSelected( + entry.getServiceId(), entry.getUrl(), entry.getName()); + } + } + dismiss(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Item handling + //////////////////////////////////////////////////////////////////////////*/ + + private void displayPlaylists(final List newPlaylists) { + this.playlists = newPlaylists; + progressBar.setVisibility(View.GONE); + if (newPlaylists.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + return; + } + recyclerView.setVisibility(View.VISIBLE); + + } + + /*////////////////////////////////////////////////////////////////////////// + // Error + //////////////////////////////////////////////////////////////////////////*/ + + protected void onError(final Throwable e) { + final Activity activity = getActivity(); + ErrorActivity.reportError(activity, e, activity.getClass(), null, ErrorActivity.ErrorInfo + .make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash)); + } + + /*////////////////////////////////////////////////////////////////////////// + // Interfaces + //////////////////////////////////////////////////////////////////////////*/ + + public interface OnSelectedListener { + void onLocalPlaylistSelected(long id, String name); + void onRemotePlaylistSelected(int serviceId, String url, String name); + } + + public interface OnCancelListener { + void onCancel(); + } + + private class SelectPlaylistAdapter + extends RecyclerView.Adapter { + @Override + public SelectPlaylistItemHolder onCreateViewHolder(final ViewGroup parent, + final int viewType) { + final View item = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.list_playlist_mini_item, parent, false); + return new SelectPlaylistItemHolder(item); + } + + @Override + public void onBindViewHolder(final SelectPlaylistItemHolder holder, final int position) { + final PlaylistLocalItem selectedItem = playlists.get(position); + + if (selectedItem instanceof PlaylistMetadataEntry) { + final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); + + holder.titleView.setText(entry.name); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + + } else if (selectedItem instanceof PlaylistRemoteEntity) { + final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); + + holder.titleView.setText(entry.getName()); + holder.view.setOnClickListener(view -> clickedItem(position)); + imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView, + DISPLAY_IMAGE_OPTIONS); + } + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + public class SelectPlaylistItemHolder extends RecyclerView.ViewHolder { + public final View view; + final ImageView thumbnailView; + final TextView titleView; + + SelectPlaylistItemHolder(final View v) { + super(v); + this.view = v; + thumbnailView = v.findViewById(R.id.itemThumbnailView); + titleView = v.findViewById(R.id.itemTitleView); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 6ebfbd73c..1b26cd529 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -34,6 +34,7 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; +import org.schabi.newpipe.settings.SelectPlaylistFragment; import org.schabi.newpipe.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipe.util.ThemeHelper; @@ -48,7 +49,7 @@ public class ChooseTabsFragment extends Fragment { private TabsManager tabsManager; - private List tabList = new ArrayList<>(); + private final List tabList = new ArrayList<>(); private ChooseTabsFragment.SelectedTabsAdapter selectedTabsAdapter; /*////////////////////////////////////////////////////////////////////////// @@ -78,10 +79,10 @@ public class ChooseTabsFragment extends Fragment { initButton(rootView); - RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); + final RecyclerView listSelectedTabs = rootView.findViewById(R.id.selectedTabs); listSelectedTabs.setLayoutManager(new LinearLayoutManager(requireContext())); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); + final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper.attachToRecyclerView(listSelectedTabs); selectedTabsAdapter = new SelectedTabsAdapter(requireContext(), itemTouchHelper); @@ -138,7 +139,7 @@ public class ChooseTabsFragment extends Fragment { private void updateTitle() { if (getActivity() instanceof AppCompatActivity) { - ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(R.string.main_page_content); } @@ -201,16 +202,33 @@ public class ChooseTabsFragment extends Fragment { switch (type) { case KIOSK: SelectKioskFragment selectKioskFragment = new SelectKioskFragment(); - selectKioskFragment.setOnSelectedLisener((serviceId, kioskId, kioskName) -> + selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) -> addTab(new Tab.KioskTab(serviceId, kioskId))); selectKioskFragment.show(requireFragmentManager(), "select_kiosk"); return; case CHANNEL: SelectChannelFragment selectChannelFragment = new SelectChannelFragment(); - selectChannelFragment.setOnSelectedLisener((serviceId, url, name) -> + selectChannelFragment.setOnSelectedListener((serviceId, url, name) -> addTab(new Tab.ChannelTab(serviceId, url, name))); selectChannelFragment.show(requireFragmentManager(), "select_channel"); return; + case PLAYLIST: + SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment(); + selectPlaylistFragment.setOnSelectedListener( + new SelectPlaylistFragment.OnSelectedListener() { + @Override + public void onLocalPlaylistSelected(final long id, final String name) { + addTab(new Tab.PlaylistTab(id, name)); + } + + @Override + public void onRemotePlaylistSelected( + final int serviceId, final String url, final String name) { + addTab(new Tab.PlaylistTab(serviceId, url, name)); + } + }); + selectPlaylistFragment.show(requireFragmentManager(), "select_playlist"); + return; default: addTab(type.getTab()); break; @@ -248,6 +266,11 @@ public class ChooseTabsFragment extends Fragment { R.attr.ic_kiosk_hot))); } break; + case PLAYLIST: + returnList.add(new ChooseTabListItem(tab.getTabId(), + getString(R.string.playlist_page_summary), + tab.getTabIconRes(context))); + break; default: if (!tabList.contains(tab)) { returnList.add(new ChooseTabListItem(context, tab)); @@ -337,7 +360,7 @@ public class ChooseTabsFragment extends Fragment { @Override public ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder onCreateViewHolder( @NonNull final ViewGroup parent, final int viewType) { - View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); + final View view = inflater.inflate(R.layout.list_choose_tabs, parent, false); return new ChooseTabsFragment.SelectedTabsAdapter.TabViewHolder(view); } @@ -393,6 +416,13 @@ public class ChooseTabsFragment extends Fragment { tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); break; + case PLAYLIST: + final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); + final String serviceName = serviceId == -1 + ? getString(R.string.local) + : NewPipe.getNameOfService(serviceId); + tabName = serviceName + "/" + tab.getTabName(requireContext()); + break; default: tabName = tab.getTabName(requireContext()); break; 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 d06b4b14e..b0511cd11 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 @@ -11,6 +11,7 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonSink; import org.schabi.newpipe.R; +import org.schabi.newpipe.database.LocalItem.LocalItemType; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -18,9 +19,11 @@ import org.schabi.newpipe.fragments.BlankFragment; import org.schabi.newpipe.fragments.list.channel.ChannelFragment; import org.schabi.newpipe.fragments.list.kiosk.DefaultKioskFragment; import org.schabi.newpipe.fragments.list.kiosk.KioskFragment; +import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment; import org.schabi.newpipe.local.bookmark.BookmarkFragment; import org.schabi.newpipe.local.feed.FeedFragment; import org.schabi.newpipe.local.history.StatisticsPlaylistFragment; +import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; @@ -33,7 +36,8 @@ import java.util.Objects; public abstract class Tab { private static final String JSON_TAB_ID_KEY = "tab_id"; - Tab() { } + Tab() { + } Tab(@NonNull final JsonObject jsonObject) { readDataFromJson(jsonObject); @@ -83,6 +87,8 @@ public abstract class Tab { return new KioskTab(jsonObject); case CHANNEL: return new ChannelTab(jsonObject); + case PLAYLIST: + return new PlaylistTab(jsonObject); } } @@ -147,7 +153,8 @@ public abstract class Tab { BOOKMARKS(new BookmarksTab()), HISTORY(new HistoryTab()), KIOSK(new KioskTab()), - CHANNEL(new ChannelTab()); + CHANNEL(new ChannelTab()), + PLAYLIST(new PlaylistTab()); private Tab tab; @@ -482,4 +489,123 @@ public abstract class Tab { return kioskId; } } + + public static class PlaylistTab extends Tab { + public static final int ID = 8; + private static final String JSON_PLAYLIST_SERVICE_ID_KEY = "playlist_service_id"; + private static final String JSON_PLAYLIST_URL_KEY = "playlist_url"; + private static final String JSON_PLAYLIST_NAME_KEY = "playlist_name"; + private static final String JSON_PLAYLIST_ID_KEY = "playlist_id"; + private static final String JSON_PLAYLIST_TYPE_KEY = "playlist_type"; + private int playlistServiceId; + private String playlistUrl; + private String playlistName; + private long playlistId; + private LocalItemType playlistType; + + private PlaylistTab() { + this(-1, ""); + } + + public PlaylistTab(final long playlistId, final String playlistName) { + this.playlistName = playlistName; + this.playlistId = playlistId; + this.playlistType = LocalItemType.PLAYLIST_LOCAL_ITEM; + this.playlistServiceId = -1; + this.playlistUrl = ""; + } + + public PlaylistTab(final int playlistServiceId, final String playlistUrl, + final String playlistName) { + this.playlistServiceId = playlistServiceId; + this.playlistUrl = playlistUrl; + this.playlistName = playlistName; + this.playlistType = LocalItemType.PLAYLIST_REMOTE_ITEM; + this.playlistId = -1; + } + + public PlaylistTab(final JsonObject jsonObject) { + super(jsonObject); + } + + @Override + public int getTabId() { + return ID; + } + + @Override + public String getTabName(final Context context) { + return playlistName; + } + + @DrawableRes + @Override + public int getTabIconRes(final Context context) { + return ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_bookmark); + } + + @Override + public Fragment getFragment(final Context context) { + if (playlistType == LocalItemType.PLAYLIST_LOCAL_ITEM) { + return LocalPlaylistFragment.getInstance(playlistId, playlistName); + + } else { // playlistType == LocalItemType.PLAYLIST_REMOTE_ITEM + return PlaylistFragment.getInstance(playlistServiceId, playlistUrl, playlistName); + } + } + + @Override + protected void writeDataToJson(final JsonSink writerSink) { + writerSink.value(JSON_PLAYLIST_SERVICE_ID_KEY, playlistServiceId) + .value(JSON_PLAYLIST_URL_KEY, playlistUrl) + .value(JSON_PLAYLIST_NAME_KEY, playlistName) + .value(JSON_PLAYLIST_ID_KEY, playlistId) + .value(JSON_PLAYLIST_TYPE_KEY, playlistType.toString()); + } + + @Override + protected void readDataFromJson(final JsonObject jsonObject) { + playlistServiceId = jsonObject.getInt(JSON_PLAYLIST_SERVICE_ID_KEY, -1); + playlistUrl = jsonObject.getString(JSON_PLAYLIST_URL_KEY, ""); + playlistName = jsonObject.getString(JSON_PLAYLIST_NAME_KEY, ""); + playlistId = jsonObject.getInt(JSON_PLAYLIST_ID_KEY, -1); + playlistType = LocalItemType.valueOf( + jsonObject.getString(JSON_PLAYLIST_TYPE_KEY, + LocalItemType.PLAYLIST_LOCAL_ITEM.toString()) + ); + } + + @Override + public boolean equals(final Object obj) { + if (!(super.equals(obj) + && Objects.equals(playlistType, ((PlaylistTab) obj).playlistType) + && Objects.equals(playlistName, ((PlaylistTab) obj).playlistName))) { + return false; // base objects are different + } + + return (playlistId == ((PlaylistTab) obj).playlistId) // local + || (playlistServiceId == ((PlaylistTab) obj).playlistServiceId // remote + && Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl)); + } + + public int getPlaylistServiceId() { + return playlistServiceId; + } + + public String getPlaylistUrl() { + return playlistUrl; + } + + public String getPlaylistName() { + return playlistName; + } + + public long getPlaylistId() { + return playlistId; + } + + public LocalItemType getPlaylistType() { + return playlistType; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 96f78ac0e..dcd751e81 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -43,7 +43,8 @@ public class DataReader { return readBuffer[readOffset++] & 0xFF; } - public long skipBytes(long amount) throws IOException { + public long skipBytes(final long byteAmount) throws IOException { + long amount = byteAmount; if (readCount < 0) { return 0; } else if (readCount == 0) { @@ -90,7 +91,10 @@ public class DataReader { return read(buffer, 0, buffer.length); } - public int read(final byte[] buffer, int offset, int count) throws IOException { + public int read(final byte[] buffer, final int off, final int c) throws IOException { + int offset = off; + int count = c; + if (readCount < 0) { return -1; } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index eb208280e..2baf8fe55 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -464,16 +464,16 @@ public class Mp4FromDashWriter { } private void initChunkTables(final TablesInfo tables, final int firstCount, - final int succesiveCount) { + final int successiveCount) { // tables.stsz holds amount of samples of the track (total) int totalSamples = (tables.stsz - firstCount); - float chunkAmount = totalSamples / (float) succesiveCount; + float chunkAmount = totalSamples / (float) successiveCount; int remainChunkOffset = (int) Math.ceil(chunkAmount); boolean remain = remainChunkOffset != (int) chunkAmount; int index = 0; tables.stsc = 1; - if (firstCount != succesiveCount) { + if (firstCount != successiveCount) { tables.stsc++; } if (remain) { @@ -488,15 +488,15 @@ public class Mp4FromDashWriter { tables.stscBEntries[index++] = firstCount; tables.stscBEntries[index++] = 1; - if (firstCount != succesiveCount) { + if (firstCount != successiveCount) { tables.stscBEntries[index++] = 2; - tables.stscBEntries[index++] = succesiveCount; + tables.stscBEntries[index++] = successiveCount; tables.stscBEntries[index++] = 1; } if (remain) { tables.stscBEntries[index++] = remainChunkOffset + 1; - tables.stscBEntries[index++] = totalSamples % succesiveCount; + tables.stscBEntries[index++] = totalSamples % successiveCount; tables.stscBEntries[index] = 1; } } @@ -640,19 +640,20 @@ public class Mp4FromDashWriter { return size; } - private byte[] makeMdat(long refSize, final boolean is64) { + private byte[] makeMdat(final long refSize, final boolean is64) { + long size = refSize; if (is64) { - refSize += 16; + size += 16; } else { - refSize += 8; + size += 8; } ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) - .putInt(is64 ? 0x01 : (int) refSize) + .putInt(is64 ? 0x01 : (int) size) .putInt(0x6D646174); // mdat if (is64) { - buffer.putLong(refSize); + buffer.putLong(size); } return buffer.array(); @@ -717,18 +718,6 @@ public class Mp4FromDashWriter { makeTrak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); } - // udta/meta/ilst/©too - auxWrite(new byte[]{ - 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, - 0x74, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, - 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, - 0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65 // "NewPipe" binary string - }); - return lengthFor(start); } @@ -850,20 +839,10 @@ public class Mp4FromDashWriter { private byte[] makeHdlr(final Hdlr hdlr) { ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ - 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72, // hdlr + 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, // hdlr 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // binary string - // "ISO Media file created in NewPipe ( - // A libre lightweight streaming frontend for Android)." - 0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, - 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, - 0x77, 0x50, 0x69, 0x70, 0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, - 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, - 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67, - 0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, - 0x41, 0x6E, - 0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E + 0x00// null string character }); buffer.position(12); @@ -899,7 +878,7 @@ public class Mp4FromDashWriter { * characteristics of sample groups. The descriptive information is any other * information needed to define or characterize the sample group. * - * ¿is replicabled this box? + * ¿is replicable this box? * NO due lacks of documentation about this box but... * most of m4a encoders and ffmpeg uses this box with dummy values (same values) */ diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index e24464dc0..00a29c7ab 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -274,30 +274,15 @@ public class OggFromWebMWriter implements Closeable { if ("A_OPUS".equals(webmTrack.codecId)) { return new byte[]{ 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x07, 0x00, 0x00, 0x00, // writing application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) }; } else if ("A_VORBIS".equals(webmTrack.codecId)) { return new byte[]{ - 0x03, // ???????? + 0x03, // ¿¿¿??? 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, // "vorbis" binary string - 0x07, 0x00, 0x00, 0x00, // writting application string size - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, // additional tags count (zero means no tags) - - /* - // whole file duration (not implemented) - 0x44,// tag string size - 0x55, 0x52, 0x41, 0x54, 0x49, 0x4F, 0x4E, 0x3D, 0x30, - 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x2E, 0x30, - 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 - */ - 0x0F, // tag string size - 0x00, 0x00, 0x00, 0x45, 0x4E, 0x43, 0x4F, - 0x44, 0x45, 0x52, 0x3D, // "ENCODER=" binary string - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ???????? + 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) + 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) }; } @@ -377,7 +362,7 @@ public class OggFromWebMWriter implements Closeable { return addPacketSegment(block.dataSize); } - private boolean addPacketSegment(int size) { + private boolean addPacketSegment(final int size) { if (size > 65025) { throw new UnsupportedOperationException("page size cannot be larger than 65025"); } @@ -396,8 +381,8 @@ public class OggFromWebMWriter implements Closeable { return false; // not enough space on the page } - for (; size > 0; size -= 255) { - segmentTable[segmentTableSize++] = (byte) Math.min(size, 255); + for (int seg = size; seg > 0; seg -= 255) { + segmentTable[segmentTableSize++] = (byte) Math.min(seg, 255); } if (extra) { @@ -419,12 +404,13 @@ public class OggFromWebMWriter implements Closeable { } } - private int calcCrc32(int initialCrc, final byte[] buffer, final int size) { + private int calcCrc32(final int initialCrc, final byte[] buffer, final int size) { + int crc = initialCrc; for (int i = 0; i < size; i++) { - int reg = (initialCrc >>> 24) & 0xff; - initialCrc = (initialCrc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; + int reg = (crc >>> 24) & 0xff; + crc = (crc << 8) ^ crc32Table[reg ^ (buffer[i] & 0xff)]; } - return initialCrc; + return crc; } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index c3cd2a2e4..02b22965d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -148,31 +148,27 @@ public class WebMWriter implements Closeable { 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, - (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, + (byte) 0xac, (byte) 0x81, + /*info offset*/ 0x43, 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, - /*tracks offset*/ 0x6a, + /*tracks offset*/ 0x56, 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, - 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, + 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, + /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, - (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 + (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, + /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 }); /* info */ listBuffer.add(new byte[]{ - 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 + 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0x8e, 0x2a, (byte) 0xd7, (byte) 0xb1 }); - listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true)); // this value MUST NOT exceed 4 bytes + // the segment duration MUST NOT exceed 4 bytes + listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true)); listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, 0x00, 0x00, 0x00, 0x00, // info.duration - - /* MuxingApp */ - 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - - /* WritingApp */ - 0x57, 0x41, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string }); /* tracks */ @@ -416,9 +412,10 @@ public class WebMWriter implements Closeable { } } - private long makeCluster(final SharpStream stream, final long timecode, long offset, + private long makeCluster(final SharpStream stream, final long timecode, final long offsetStart, final boolean create) throws IOException { ClusterInfo cluster; + long offset = offsetStart; if (offset > 0) { // save the size of the previous cluster (maximum 256 MiB) @@ -449,7 +446,7 @@ public class WebMWriter implements Closeable { } private void makeEBML(final SharpStream stream) throws IOException { - // deafult values + // default values dump(new byte[]{ 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, @@ -588,8 +585,10 @@ public class WebMWriter implements Closeable { return lengthFor(buffer); } - private void makeEbmlVoid(final SharpStream out, int size, final boolean wipe) + private void makeEbmlVoid(final SharpStream out, final int amount, final boolean wipe) throws IOException { + int size = amount; + /* ebml void */ outByteBuffer.putShort(0, (short) 0xec20); outByteBuffer.putShort(2, (short) (size - 4)); 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 cd5992fb4..9b8b2494e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java @@ -33,6 +33,7 @@ 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.Page; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; @@ -87,14 +88,13 @@ public final class ExtractorHelper { final String searchString, final List contentFilter, final String sortFilter, - final String pageUrl) { + final Page page) { checkServiceId(serviceId); return Single.fromCallable(() -> SearchInfo.getMoreItems(NewPipe.getService(serviceId), NewPipe.getService(serviceId) .getSearchQHFactory() - .fromQuery(searchString, contentFilter, sortFilter), - pageUrl)); + .fromQuery(searchString, contentFilter, sortFilter), page)); } @@ -125,10 +125,10 @@ public final class ExtractorHelper { } public static Single getMoreChannelItems(final int serviceId, final String url, - final String nextStreamsUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + ChannelInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } public static Single> getFeedInfoFallbackToChannelInfo( @@ -157,10 +157,10 @@ public final class ExtractorHelper { public static Single getMoreCommentItems(final int serviceId, final CommentsInfo info, - final String nextPageUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPageUrl)); + CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage)); } public static Single getPlaylistInfo(final int serviceId, final String url, @@ -172,10 +172,10 @@ public final class ExtractorHelper { } public static Single getMorePlaylistItems(final int serviceId, final String url, - final String nextStreamsUrl) { + final Page nextPage) { checkServiceId(serviceId); return Single.fromCallable(() -> - PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl)); + PlaylistInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } public static Single getKioskInfo(final int serviceId, final String url, @@ -184,12 +184,10 @@ public final class ExtractorHelper { Single.fromCallable(() -> KioskInfo.getInfo(NewPipe.getService(serviceId), url))); } - public static Single getMoreKioskItems(final int serviceId, - final String url, - final String nextStreamsUrl) { + public static Single getMoreKioskItems(final int serviceId, final String url, + final Page nextPage) { return Single.fromCallable(() -> - KioskInfo.getMoreItems(NewPipe.getService(serviceId), - url, nextStreamsUrl)); + KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextPage)); } /*////////////////////////////////////////////////////////////////////////// @@ -240,8 +238,8 @@ public final class ExtractorHelper { * @param infoType the {@link InfoItem.InfoType} of the item * @return a {@link Single} that loads the item */ - public static Maybe loadFromCache(final int serviceId, final String url, - final InfoItem.InfoType infoType) { + private static Maybe loadFromCache(final int serviceId, final String url, + final InfoItem.InfoType infoType) { checkServiceId(serviceId); return Maybe.defer(() -> { //noinspection unchecked diff --git a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java index 0bf731a98..0ec2d571d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/ShareUtils.java @@ -1,10 +1,13 @@ package org.schabi.newpipe.util; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.widget.Toast; import org.schabi.newpipe.R; @@ -78,4 +81,27 @@ public final class ShareUtils { context.startActivity(Intent.createChooser( intent, context.getString(R.string.share_dialog_title))); } + + /** + * Copy the text to clipboard, and indicate to the user whether the operation was completed + * successfully using a Toast. + * + * @param context the context to use + * @param text the text to copy + */ + public static void copyToClipboard(final Context context, final String text) { + final ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + + if (clipboardManager == null) { + Toast.makeText(context, + R.string.permission_denied, + Toast.LENGTH_LONG).show(); + return; + } + + clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); + Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT) + .show(); + } } diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index c852da65e..84a29e0c8 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -111,7 +111,7 @@ tools:ignore="RtlHardcoded"> @@ -161,7 +161,7 @@ android:visibility="invisible"/> @@ -185,8 +185,8 @@ android:orientation="horizontal" tools:ignore="RtlHardcoded"> - + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" + android:scaleType="fitXY" + android:src="@drawable/exo_controls_previous" + android:tint="?attr/colorAccent" + tools:ignore="ContentDescription" /> - + android:background="?attr/selectableItemBackgroundBorderless" + android:clickable="true" + android:focusable="true" + android:scaleType="fitXY" + android:src="@drawable/exo_controls_next" + android:tint="?attr/colorAccent" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/activity_error.xml b/app/src/main/res/layout/activity_error.xml index c47077c73..12237f918 100644 --- a/app/src/main/res/layout/activity_error.xml +++ b/app/src/main/res/layout/activity_error.xml @@ -118,11 +118,31 @@ android:inputType="" />