diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d08ae9051..d5fdc97e0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,6 +22,7 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se * NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register. * Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki. +* NewPipe uses the [PrettyTime](https://github.com/ocpsoft/prettytime) library to display localized versions of dates and times. It needs to be translated, too. Read [these instructions to add a new language](https://www.ocpsoft.org/prettytime/#section-14) and [this issue](https://github.com/TeamNewPipe/NewPipe/issues/9134) for more info. ## Code contribution diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 52b2a4241..31ef92c44 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Suggest an idea for this project -labels: [enhancement, needs triage] +labels: [feature request, needs triage] body: - type: markdown attributes: diff --git a/app/build.gradle b/app/build.gradle index 9d941d5a7..206c830e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,16 +8,16 @@ plugins { } android { - compileSdk 31 - buildToolsVersion '31.0.0' + compileSdk 32 + namespace 'org.schabi.newpipe' defaultConfig { applicationId "org.schabi.newpipe" resValue "string", "app_name", "NewPipe" minSdk 21 targetSdk 29 - versionCode 990 - versionName "0.24.0" + versionCode 991 + versionName "0.24.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -187,7 +187,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:eb07d70a2ce03bee3cc74fc33b2e4173e1c21436' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ @@ -198,7 +198,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" /** AndroidX **/ - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.core:core-ktx:1.8.0' @@ -271,7 +271,7 @@ dependencies { implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" // Date and time formatting - implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final" + implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final" /** Debugging **/ // Memory leak detection diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 5cc2fa66a..02a0f6c74 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,8 +1,6 @@ - + @@ -14,6 +13,9 @@ + + diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index 50a3984e3..c1eec1526 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -78,6 +78,7 @@ class AboutActivity : AppCompatActivity() { aboutDonationLink.openLink(R.string.donation_url) aboutWebsiteLink.openLink(R.string.website_url) aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url) + faqLink.openLink(R.string.faq_url) return root } } diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index b2b3d18a6..968d0c88f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -48,7 +48,10 @@ abstract class FeedDAO { ON s.uid = f.stream_id LEFT JOIN feed_group_subscription_join fgs - ON fgs.subscription_id = f.subscription_id + ON ( + :groupId <> ${FeedGroupEntity.GROUP_ALL_ID} + AND fgs.subscription_id = f.subscription_id + ) WHERE ( :groupId = ${FeedGroupEntity.GROUP_ALL_ID} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 0e64e8b48..2975fe43a 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -145,6 +145,12 @@ public class DownloadDialog extends DialogFragment // Instance creation //////////////////////////////////////////////////////////////////////////*/ + public DownloadDialog() { + // Just an empty default no-arg ctor to keep Fragment.instantiate() happy + // otherwise InstantiationException will be thrown when fragment is recreated + // TODO: Maybe use a custom FragmentFactory instead? + } + /** * Create a new download dialog with the video, audio and subtitle streams from the provided * stream info. Video streams and video-only streams will be put into a single list menu, @@ -153,7 +159,7 @@ public class DownloadDialog extends DialogFragment * @param context the context to use just to obtain preferences and strings (will not be stored) * @param info the info from which to obtain downloadable streams and other info (e.g. title) */ - public DownloadDialog(final Context context, @NonNull final StreamInfo info) { + public DownloadDialog(@NonNull final Context context, @NonNull final StreamInfo info) { this.currentInfo = info; // TODO: Adapt this code when the downloader support other types of stream deliveries diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index 15343f53d..b41d3997e 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt @@ -30,6 +30,7 @@ import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.isInterruptedCaused import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.util.external_communication.ShareUtils import java.util.concurrent.TimeUnit class ErrorPanelHelper( @@ -52,6 +53,8 @@ class ErrorPanelHelper( errorPanelRoot.findViewById(R.id.error_action_button) private val errorRetryButton: Button = errorPanelRoot.findViewById(R.id.error_retry_button) + private val errorOpenInBrowserButton: Button = + errorPanelRoot.findViewById(R.id.error_open_in_browser) private var errorDisposable: Disposable? = null @@ -69,6 +72,7 @@ class ErrorPanelHelper( errorServiceExplanationTextView.isVisible = false errorActionButton.isVisible = false errorRetryButton.isVisible = false + errorOpenInBrowserButton.isVisible = false } fun showError(errorInfo: ErrorInfo) { @@ -99,6 +103,7 @@ class ErrorPanelHelper( } errorRetryButton.isVisible = true + showAndSetOpenInBrowserButtonAction(errorInfo) } else if (errorInfo.throwable is AccountTerminatedException) { errorTextView.setText(R.string.account_terminated) @@ -128,6 +133,7 @@ class ErrorPanelHelper( // show retry button only for content which is not unavailable or unsupported errorRetryButton.isVisible = true } + showAndSetOpenInBrowserButtonAction(errorInfo) } setRootVisible() @@ -145,6 +151,15 @@ class ErrorPanelHelper( errorActionButton.setOnClickListener(listener) } + fun showAndSetOpenInBrowserButtonAction( + errorInfo: ErrorInfo + ) { + errorOpenInBrowserButton.isVisible = true + errorOpenInBrowserButton.setOnClickListener { + ShareUtils.openUrlInBrowser(context, errorInfo.request, true) + } + } + fun showTextError(errorString: String) { ensureDefaultVisibility() 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 09e085791..580585d4e 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 @@ -248,6 +248,7 @@ public final class VideoDetailFragment autoPlayEnabled = true; // forcefully start playing openVideoPlayerAutoFullscreen(); } + updateOverlayPlayQueueButtonVisibility(); } @Override @@ -337,6 +338,8 @@ public final class VideoDetailFragment activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); + updateOverlayPlayQueueButtonVisibility(); + setupBrightness(); if (tabSettingsChanged) { @@ -526,6 +529,9 @@ public final class VideoDetailFragment case R.id.overlay_buttons_layout: bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); break; + case R.id.overlay_play_queue_button: + NavigationHelper.openPlayQueue(getContext()); + break; case R.id.overlay_play_pause_button: if (playerIsNotStopped()) { player.playPause(); @@ -684,6 +690,7 @@ public final class VideoDetailFragment binding.overlayMetadataLayout.setOnClickListener(this); binding.overlayMetadataLayout.setOnLongClickListener(this); binding.overlayButtonsLayout.setOnClickListener(this); + binding.overlayPlayQueueButton.setOnClickListener(this); binding.overlayCloseButton.setOnClickListener(this); binding.overlayPlayPauseButton.setOnClickListener(this); @@ -1816,6 +1823,14 @@ public final class VideoDetailFragment + title + "], playQueue = [" + playQueue + "]"); } + // Register broadcast receiver to listen to playQueue changes + // and hide the overlayPlayQueueButton when the playQueue is empty / destroyed. + if (playQueue != null && playQueue.getBroadcastReceiver() != null) { + playQueue.getBroadcastReceiver().subscribe( + event -> updateOverlayPlayQueueButtonVisibility() + ); + } + // This should be the only place where we push data to stack. // It will allow to have live instance of PlayQueue with actual information about // deleted/added items inside Channel/Playlist queue and makes possible to have @@ -1922,6 +1937,7 @@ public final class VideoDetailFragment currentInfo.getUploaderName(), currentInfo.getThumbnailUrl()); } + updateOverlayPlayQueueButtonVisibility(); } @Override @@ -2388,6 +2404,18 @@ public final class VideoDetailFragment }); } + private void updateOverlayPlayQueueButtonVisibility() { + final boolean isPlayQueueEmpty = + player == null // no player => no play queue :) + || player.getPlayQueue() == null + || player.getPlayQueue().isEmpty(); + if (binding != null) { + // binding is null when rotating the device... + binding.overlayPlayQueueButton.setVisibility( + isPlayQueueEmpty ? View.GONE : View.VISIBLE); + } + } + private void updateOverlayData(@Nullable final String overlayTitle, @Nullable final String uploader, @Nullable final String thumbnailUrl) { @@ -2426,6 +2454,7 @@ public final class VideoDetailFragment binding.overlayMetadataLayout.setClickable(enable); binding.overlayMetadataLayout.setLongClickable(enable); binding.overlayButtonsLayout.setClickable(enable); + binding.overlayPlayQueueButton.setClickable(enable); binding.overlayPlayPauseButton.setClickable(enable); binding.overlayCloseButton.setClickable(enable); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 9e7cb757c..633ba5d78 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -5,7 +5,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingSc import android.content.Context; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; @@ -31,6 +30,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.StateSaver; +import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.views.SuperScrollLayoutManager; import java.util.List; @@ -476,15 +476,6 @@ public abstract class BaseListFragment extends BaseStateFragment } protected boolean isGridLayout() { - final String listMode = PreferenceManager.getDefaultSharedPreferences(activity) - .getString(getString(R.string.list_view_mode_key), - getString(R.string.list_view_mode_value)); - if ("auto".equals(listMode)) { - final Configuration configuration = getResources().getConfiguration(); - return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); - } else { - return "grid".equals(listMode); - } + return ThemeHelper.shouldUseGridLayout(activity); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt index 9fb4bfbb3..869bf6f48 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentAdapter.kt @@ -61,5 +61,6 @@ class StreamSegmentAdapter( interface StreamSegmentListener { fun onItemClick(item: StreamSegmentItem, seconds: Int) + fun onItemLongClick(item: StreamSegmentItem, seconds: Int) } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt index f233c7627..5fc8aa684 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt @@ -41,6 +41,7 @@ class StreamSegmentItem( viewHolder.root.findViewById(R.id.textViewStartSeconds).text = Localization.getDurationString(item.startTimeSeconds.toLong()) viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) } + viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true } viewHolder.root.isSelected = isSelected } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java index 1265e9767..c67880d0e 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -112,12 +112,19 @@ public enum StreamDialogDefaultEntry { ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), item.getThumbnailUrl())), + /** + * Opens a {@link DownloadDialog} after fetching some stream info. + * If the user quits the current fragment, it will not open a DownloadDialog. + */ DOWNLOAD(R.string.download, (fragment, item) -> fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(), item.getUrl(), info -> { - final DownloadDialog downloadDialog = - new DownloadDialog(fragment.requireContext(), info); - downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog"); + if (fragment.getContext() != null) { + final DownloadDialog downloadDialog = + new DownloadDialog(fragment.requireContext(), info); + downloadDialog.show(fragment.getChildFragmentManager(), + "downloadDialog"); + } }) ), diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index b900750a8..92e37afd8 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -12,6 +12,7 @@ import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.text.util.LinkifyCompat; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorUtil; @@ -27,7 +28,7 @@ import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.TimestampExtractor; -import java.util.regex.Matcher; +import java.util.Objects; public class CommentsMiniInfoItemHolder extends InfoItemHolder { private static final String TAG = "CommentsMiniIIHolder"; @@ -39,7 +40,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { private final int commentVerticalPadding; private final RelativeLayout itemRoot; - public final ImageView itemThumbnailView; + private final ImageView itemThumbnailView; private final TextView itemContentView; private final TextView itemLikesCountView; private final TextView itemPublishedTime; @@ -47,27 +48,6 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { private String commentText; private String streamUrl; - private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() { - @Override - public String transformUrl(final Matcher match, final String url) { - try { - final TimestampExtractor.TimestampMatchDTO timestampMatchDTO = - TimestampExtractor.getTimestampFromMatcher(match, commentText); - - if (timestampMatchDTO == null) { - return url; - } - - return streamUrl + url.replace( - match.group(0), - "#timestamp=" + timestampMatchDTO.seconds()); - } catch (final Exception ex) { - Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex); - return url; - } - } - }; - CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId, final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -115,7 +95,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { itemContentView.setLines(COMMENT_DEFAULT_LINES); commentText = item.getCommentText(); - itemContentView.setText(commentText); + itemContentView.setText(commentText, TextView.BufferType.SPANNABLE); itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); if (itemContentView.getLineCount() == 0) { @@ -243,14 +223,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { } private void linkify() { - Linkify.addLinks( - itemContentView, - Linkify.WEB_URLS); - Linkify.addLinks( - itemContentView, - TimestampExtractor.TIMESTAMPS_PATTERN, - null, - null, - timestampLink); + LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS); + LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null, + (match, url) -> { + try { + final var timestampMatch = TimestampExtractor + .getTimestampFromMatcher(match, commentText); + if (timestampMatch == null) { + return url; + } + return streamUrl + url.replace(Objects.requireNonNull(match.group(0)), + "#timestamp=" + timestampMatch.seconds()); + } catch (final Exception ex) { + Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex); + return url; + } + }); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index c9f926f06..b3442d3dc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -40,6 +40,7 @@ import androidx.annotation.Nullable import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.edit +import androidx.core.math.MathUtils import androidx.core.os.bundleOf import androidx.core.view.MenuItemCompat import androidx.core.view.isVisible @@ -603,7 +604,7 @@ class FeedFragment : BaseStateFragment() { // state until the user scrolls them out of the visible area which causes a update/bind-call groupAdapter.notifyItemRangeChanged( 0, - minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount)) + MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount) ) if (highlightCount > 0) { diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 11d54f1ef..d98ce4121 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -11,6 +11,7 @@ import android.os.Parcelable; import android.text.InputType; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -22,6 +23,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; import androidx.viewbinding.ViewBinding; @@ -34,7 +36,6 @@ import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.databinding.DialogEditTextBinding; import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding; import org.schabi.newpipe.databinding.PlaylistControlBinding; @@ -55,7 +56,6 @@ import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,7 +63,6 @@ import java.util.stream.Collectors; import icepick.State; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; @@ -309,7 +308,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment> getPlaylistObserver() { - return new Subscriber>() { + return new Subscriber<>() { @Override public void onSubscribe(final Subscription s) { showLoading(); @@ -395,31 +394,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment playlist) -> { - // Playlist data - final Iterator playlistIter = playlist.iterator(); - - // History data - final HistoryRecordManager recordManager = - new HistoryRecordManager(getContext()); - final Iterator historyIter = recordManager - .getStreamHistorySortedById().blockingFirst().iterator(); - + final var recordManager = new HistoryRecordManager(getContext()); + final var historyIdsMaybe = recordManager.getStreamHistorySortedById() + .firstElement() + // already sorted by ^ getStreamHistorySortedById(), binary search can be used + .map(historyList -> historyList.stream().map(StreamHistoryEntry::getStreamId) + .collect(Collectors.toList())); + final var streamsMaybe = playlistManager.getPlaylistStreams(playlistId) + .firstElement() + .zipWith(historyIdsMaybe, (playlist, historyStreamIds) -> { // Remove Watched, Functionality data final List notWatchedItems = new ArrayList<>(); boolean thumbnailVideoRemoved = false; - // already sorted by ^ getStreamHistorySortedById(), binary search can be used - final ArrayList historyStreamIds = new ArrayList<>(); - while (historyIter.hasNext()) { - historyStreamIds.add(historyIter.next().getStreamId()); - } - if (removePartiallyWatched) { - while (playlistIter.hasNext()) { - final PlaylistStreamEntry playlistItem = playlistIter.next(); + for (final var playlistItem : playlist) { final int indexInHistory = Collections.binarySearch(historyStreamIds, playlistItem.getStreamId()); @@ -432,14 +421,15 @@ public class LocalPlaylistFragment extends BaseLocalListFragment streamStatesIter = recordManager - .loadLocalStreamStateBatch(playlist).blockingGet().iterator(); + final var streamStates = recordManager + .loadLocalStreamStateBatch(playlist).blockingGet(); + + for (int i = 0; i < playlist.size(); i++) { + final var playlistItem = playlist.get(i); + final var streamStateEntity = streamStates.get(i); - while (playlistIter.hasNext()) { - final PlaylistStreamEntry playlistItem = playlistIter.next(); final int indexInHistory = Collections.binarySearch(historyStreamIds, playlistItem.getStreamId()); - final StreamStateEntity streamStateEntity = streamStatesIter.next(); final long duration = playlistItem.toStreamInfoItem().getDuration(); if (indexInHistory < 0 || (streamStateEntity != null @@ -453,19 +443,19 @@ public class LocalPlaylistFragment extends BaseLocalListFragment(notWatchedItems, thumbnailVideoRemoved); + }); + + disposables.add(streamsMaybe.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(flow -> { - final List notWatchedItems = - (List) flow.blockingFirst(); - final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast(); + final List notWatchedItems = flow.first; + final boolean thumbnailVideoRemoved = flow.second; itemListAdapter.clearStreamItemList(); itemListAdapter.addItems(notWatchedItems); saveChanges(); - if (thumbnailVideoRemoved) { updateThumbnailUrl(); } @@ -503,13 +493,18 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); - playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); - playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); - + playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> { + NavigationHelper.playOnMainPlayer(activity, getPlayQueue()); + showHoldToAppendTipIfNeeded(); + }); + playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> { + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false); + showHoldToAppendTipIfNeeded(); + }); + playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> { + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false); + showHoldToAppendTipIfNeeded(); + }); playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> { NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP); return true; @@ -523,6 +518,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { private val disposables: CompositeDisposable = CompositeDisposable() private val groupAdapter = GroupAdapter>() - private val feedGroupsSection = Section() - private var feedGroupsCarousel: FeedGroupCarouselItem? = null - private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem + private lateinit var carouselAdapter: GroupAdapter> + private lateinit var feedGroupsCarousel: FeedGroupCarouselItem + private lateinit var feedGroupsSortMenuItem: GroupsHeader private val subscriptionsSection = Section() private val requestExportLauncher = @@ -90,7 +91,7 @@ class SubscriptionFragment : BaseStateFragment() { @State @JvmField - var feedGroupsListState: Parcelable? = null + var feedGroupsCarouselState: Parcelable? = null init { setHasOptionsMenu(true) @@ -100,11 +101,6 @@ class SubscriptionFragment : BaseStateFragment() { // Fragment LifeCycle // ///////////////////////////////////////////////////////////////////////// - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setupInitialLayout() - } - override fun onAttach(context: Context) { super.onAttach(context) subscriptionManager = SubscriptionManager(requireContext()) @@ -117,7 +113,7 @@ class SubscriptionFragment : BaseStateFragment() { override fun onPause() { super.onPause() itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState() - feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() + feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState() } override fun onDestroy() { @@ -184,7 +180,7 @@ class SubscriptionFragment : BaseStateFragment() { menuItem: MenuItem, onClick: Runnable ): MenuItem { - menuItem.setOnMenuItemClickListener { _ -> + menuItem.setOnMenuItemClickListener { onClick.run() true } @@ -245,51 +241,6 @@ class SubscriptionFragment : BaseStateFragment() { // Fragment Views // //////////////////////////////////////////////////////////////////////// - private fun setupInitialLayout() { - Section().apply { - val carouselAdapter = GroupAdapter>() - - carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS)) - carouselAdapter.add(feedGroupsSection) - carouselAdapter.add(FeedGroupAddItem()) - - carouselAdapter.setOnItemClickListener { item, _ -> - listenerFeedGroups.selected(item) - } - carouselAdapter.setOnItemLongClickListener { item, _ -> - if (item is FeedGroupCardItem) { - if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) { - return@setOnItemLongClickListener false - } - } - listenerFeedGroups.held(item) - return@setOnItemLongClickListener true - } - - feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter) - feedGroupsSortMenuItem = HeaderWithMenuItem( - getString(R.string.feed_groups_header_title), - R.drawable.ic_sort, - menuItemOnClickListener = ::openReorderDialog - ) - add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) - - groupAdapter.add(this) - } - - subscriptionsSection.setPlaceholder(EmptyPlaceholderItem()) - subscriptionsSection.setHideWhenEmpty(true) - - groupAdapter.add( - Section( - HeaderWithMenuItem( - getString(R.string.tab_subscriptions) - ), - listOf(subscriptionsSection) - ) - ) - } - override fun initViews(rootView: View, savedInstanceState: Bundle?) { super.initViews(rootView, savedInstanceState) _binding = FragmentSubscriptionBinding.bind(rootView) @@ -299,10 +250,81 @@ class SubscriptionFragment : BaseStateFragment() { spanSizeLookup = groupAdapter.spanSizeLookup } binding.itemsList.adapter = groupAdapter + binding.itemsList.itemAnimator = null - viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java) + viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java] viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) } - viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) } + viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { + it?.let { (groups, listViewMode) -> + handleFeedGroups(groups, listViewMode) + } + } + + setupInitialLayout() + } + + private fun setupInitialLayout() { + Section().apply { + carouselAdapter = GroupAdapter>() + + carouselAdapter.setOnItemClickListener { item, _ -> + when (item) { + is FeedGroupCardItem -> + NavigationHelper.openFeedFragment(fm, item.groupId, item.name) + is FeedGroupCardGridItem -> + NavigationHelper.openFeedFragment(fm, item.groupId, item.name) + is FeedGroupAddNewItem -> + FeedGroupDialog.newInstance().show(fm, null) + is FeedGroupAddNewGridItem -> + FeedGroupDialog.newInstance().show(fm, null) + } + } + carouselAdapter.setOnItemLongClickListener { item, _ -> + if ((item is FeedGroupCardItem && item.groupId == GROUP_ALL_ID) || + (item is FeedGroupCardGridItem && item.groupId == GROUP_ALL_ID) + ) { + return@setOnItemLongClickListener false + } + + when (item) { + is FeedGroupCardItem -> + FeedGroupDialog.newInstance(item.groupId).show(fm, null) + is FeedGroupCardGridItem -> + FeedGroupDialog.newInstance(item.groupId).show(fm, null) + } + return@setOnItemLongClickListener true + } + + feedGroupsCarousel = FeedGroupCarouselItem( + carouselAdapter = carouselAdapter, + listViewMode = viewModel.getListViewMode() + ) + + feedGroupsSortMenuItem = GroupsHeader( + title = getString(R.string.feed_groups_header_title), + onSortClicked = ::openReorderDialog, + onToggleListViewModeClicked = ::toggleListViewMode, + listViewMode = viewModel.getListViewMode(), + ) + + add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) + groupAdapter.clear() + groupAdapter.add(this) + } + + subscriptionsSection.setPlaceholder(EmptyPlaceholderItem()) + subscriptionsSection.setHideWhenEmpty(true) + + groupAdapter.add( + Section( + Header(getString(R.string.tab_subscriptions)), + listOf(subscriptionsSection) + ) + ) + } + + private fun toggleListViewMode() { + viewModel.setListViewMode(!viewModel.getListViewMode()) } private fun showLongTapDialog(selectedItem: ChannelInfoItem) { @@ -346,21 +368,6 @@ class SubscriptionFragment : BaseStateFragment() { override fun doInitialLoadLogic() = Unit override fun startLoading(forceLoad: Boolean) = Unit - private val listenerFeedGroups = object : OnClickGesture> { - override fun selected(selectedItem: Item<*>?) { - when (selectedItem) { - is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name) - is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null) - } - } - - override fun held(selectedItem: Item<*>?) { - when (selectedItem) { - is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null) - } - } - } - private val listenerChannelItem = object : OnClickGesture { override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( fm, @@ -402,16 +409,38 @@ class SubscriptionFragment : BaseStateFragment() { } } - private fun handleFeedGroups(groups: List) { - feedGroupsSection.update(groups) - - if (feedGroupsListState != null) { - feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState) - feedGroupsListState = null + private fun handleFeedGroups(groups: List, listViewMode: Boolean) { + if (feedGroupsCarouselState != null) { + feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState) + feedGroupsCarouselState = null } - feedGroupsSortMenuItem.showMenuItem = groups.size > 1 - binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) } + binding.itemsList.post { + if (context == null) { + // since this part was posted to the next UI cycle, the fragment might have been + // removed in the meantime + return@post + } + + feedGroupsCarousel.listViewMode = listViewMode + feedGroupsSortMenuItem.showSortButton = groups.size > 1 + feedGroupsSortMenuItem.listViewMode = listViewMode + feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE) + feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS) + + // update items here to prevent flickering + carouselAdapter.apply { + clear() + if (listViewMode) { + add(FeedGroupAddNewItem()) + add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS)) + } else { + add(FeedGroupAddNewGridItem()) + add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.RSS)) + } + addAll(groups) + } + } } // ///////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt index da009e1a0..914299c78 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt @@ -5,25 +5,45 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.xwray.groupie.Group +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.subscription.item.ChannelItem +import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT +import org.schabi.newpipe.util.ThemeHelper import java.util.concurrent.TimeUnit class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) private var subscriptionManager = SubscriptionManager(application) - private val mutableStateLiveData = MutableLiveData() - private val mutableFeedGroupsLiveData = MutableLiveData>() - val stateLiveData: LiveData = mutableStateLiveData - val feedGroupsLiveData: LiveData> = mutableFeedGroupsLiveData + // true -> list view, false -> grid view + private val listViewMode = BehaviorProcessor.createDefault( + !ThemeHelper.shouldUseGridLayout(application) + ) + private val listViewModeFlowable = listViewMode.distinctUntilChanged() - private var feedGroupItemsDisposable = feedDatabaseManager.groups() + private val mutableStateLiveData = MutableLiveData() + private val mutableFeedGroupsLiveData = MutableLiveData, Boolean>>() + val stateLiveData: LiveData = mutableStateLiveData + val feedGroupsLiveData: LiveData, Boolean>> = mutableFeedGroupsLiveData + + private var feedGroupItemsDisposable = Flowable + .combineLatest( + feedDatabaseManager.groups(), + listViewModeFlowable, + ::Pair + ) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) - .map { it.map(::FeedGroupCardItem) } + .map { (feedGroups, listViewMode) -> + Pair( + feedGroups.map(if (listViewMode) ::FeedGroupCardItem else ::FeedGroupCardGridItem), + listViewMode + ) + } .subscribeOn(Schedulers.io()) .subscribe( { mutableFeedGroupsLiveData.postValue(it) }, @@ -45,6 +65,14 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica feedGroupItemsDisposable.dispose() } + fun setListViewMode(newListViewMode: Boolean) { + listViewMode.onNext(newListViewMode) + } + + fun getListViewMode(): Boolean { + return listViewMode.value ?: true + } + sealed class SubscriptionState { data class LoadedState(val subscriptions: List) : SubscriptionState() data class ErrorState(val error: Throwable? = null) : SubscriptionState() diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt deleted file mode 100644 index 7b7490eaa..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/decoration/FeedGroupCarouselDecoration.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.schabi.newpipe.local.subscription.decoration - -import android.content.Context -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import org.schabi.newpipe.R - -class FeedGroupCarouselDecoration(context: Context) : RecyclerView.ItemDecoration() { - - private val marginStartEnd: Int - private val marginTopBottom: Int - private val marginBetweenItems: Int - - init { - with(context.resources) { - marginStartEnd = getDimensionPixelOffset(R.dimen.feed_group_carousel_start_end_margin) - marginTopBottom = getDimensionPixelOffset(R.dimen.feed_group_carousel_top_bottom_margin) - marginBetweenItems = getDimensionPixelOffset(R.dimen.feed_group_carousel_between_items_margin) - } - } - - override fun getItemOffsets(outRect: Rect, child: View, parent: RecyclerView, state: RecyclerView.State) { - val childAdapterPosition = parent.getChildAdapterPosition(child) - val childAdapterCount = parent.adapter?.itemCount ?: 0 - - outRect.set(marginBetweenItems, marginTopBottom, 0, marginTopBottom) - - if (childAdapterPosition == 0) { - outRect.left = marginStartEnd - } else if (childAdapterPosition == childAdapterCount - 1) { - outRect.right = marginStartEnd - } - } -} 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 379b4c0d7..4b3c4ccc0 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 @@ -124,11 +124,13 @@ class FeedGroupDialog : DialogFragment(), BackPressable { viewModel = ViewModelProvider( this, - FeedGroupDialogViewModel.Factory( + FeedGroupDialogViewModel.getFactory( requireContext(), - groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped + groupId, + subscriptionsCurrentSearchQuery, + subscriptionsShowOnlyUngrouped ) - ).get(FeedGroupDialogViewModel::class.java) + )[FeedGroupDialogViewModel::class.java] viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) { 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 dfdb2b47a..eff1a4400 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 @@ -4,7 +4,8 @@ import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.Disposable @@ -115,18 +116,18 @@ class FeedGroupDialogViewModel( 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 + companion object { + fun getFactory( + context: Context, + groupId: Long, + initialQuery: String, + initialShowOnlyUngrouped: Boolean + ) = viewModelFactory { + initializer { + FeedGroupDialogViewModel( + context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped + ) + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewGridItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewGridItem.kt new file mode 100644 index 000000000..a2870b849 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewGridItem.kt @@ -0,0 +1,14 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View +import com.xwray.groupie.viewbinding.BindableItem +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.FeedGroupAddNewGridItemBinding + +class FeedGroupAddNewGridItem : BindableItem() { + override fun getLayout(): Int = R.layout.feed_group_add_new_grid_item + override fun initializeViewBinding(view: View) = FeedGroupAddNewGridItemBinding.bind(view) + override fun bind(viewBinding: FeedGroupAddNewGridItemBinding, position: Int) { + // this is a static item, nothing to do here + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewItem.kt similarity index 75% rename from app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt rename to app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewItem.kt index 434b4f29a..e06e578f8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupAddNewItem.kt @@ -5,8 +5,10 @@ import com.xwray.groupie.viewbinding.BindableItem import org.schabi.newpipe.R import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding -class FeedGroupAddItem : BindableItem() { +class FeedGroupAddNewItem : BindableItem() { override fun getLayout(): Int = R.layout.feed_group_add_new_item - override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) {} override fun initializeViewBinding(view: View) = FeedGroupAddNewItemBinding.bind(view) + override fun bind(viewBinding: FeedGroupAddNewItemBinding, position: Int) { + // this is a static item, nothing to do here + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt new file mode 100644 index 000000000..5a9d6887b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCardGridItem.kt @@ -0,0 +1,32 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View +import com.xwray.groupie.viewbinding.BindableItem +import org.schabi.newpipe.R +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.databinding.FeedGroupCardGridItemBinding +import org.schabi.newpipe.local.subscription.FeedGroupIcon + +data class FeedGroupCardGridItem( + val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + val name: String, + val icon: FeedGroupIcon, +) : BindableItem() { + constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon) + + override fun getId(): Long { + return when (groupId) { + FeedGroupEntity.GROUP_ALL_ID -> super.getId() + else -> groupId + } + } + + override fun getLayout(): Int = R.layout.feed_group_card_grid_item + + override fun bind(viewBinding: FeedGroupCardGridItemBinding, position: Int) { + viewBinding.title.text = name + viewBinding.icon.setImageResource(icon.getDrawableRes()) + } + + override fun initializeViewBinding(view: View) = FeedGroupCardGridItemBinding.bind(view) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt index 44af16280..bf9f9072f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupCarouselItem.kt @@ -1,60 +1,82 @@ package org.schabi.newpipe.local.subscription.item -import android.content.Context import android.os.Parcelable import android.view.View +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.xwray.groupie.GroupAdapter import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.GroupieViewHolder import org.schabi.newpipe.R import org.schabi.newpipe.databinding.FeedItemCarouselBinding -import org.schabi.newpipe.local.subscription.decoration.FeedGroupCarouselDecoration +import org.schabi.newpipe.util.DeviceUtils +import org.schabi.newpipe.util.ThemeHelper.getGridSpanCount class FeedGroupCarouselItem( - context: Context, - private val carouselAdapter: GroupAdapter> + private val carouselAdapter: GroupAdapter>, + var listViewMode: Boolean ) : BindableItem() { - private val feedGroupCarouselDecoration = FeedGroupCarouselDecoration(context) + companion object { + const val PAYLOAD_UPDATE_LIST_VIEW_MODE = 2 + } - private var linearLayoutManager: LinearLayoutManager? = null + private var carouselLayoutManager: LinearLayoutManager? = null private var listState: Parcelable? = null override fun getLayout() = R.layout.feed_item_carousel fun onSaveInstanceState(): Parcelable? { - listState = linearLayoutManager?.onSaveInstanceState() + listState = carouselLayoutManager?.onSaveInstanceState() return listState } fun onRestoreInstanceState(state: Parcelable?) { - linearLayoutManager?.onRestoreInstanceState(state) + carouselLayoutManager?.onRestoreInstanceState(state) listState = state } override fun initializeViewBinding(view: View): FeedItemCarouselBinding { - val viewHolder = FeedItemCarouselBinding.bind(view) + val viewBinding = FeedItemCarouselBinding.bind(view) + updateViewMode(viewBinding) + return viewBinding + } - linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false) - - viewHolder.recyclerView.apply { - layoutManager = linearLayoutManager - adapter = carouselAdapter - addItemDecoration(feedGroupCarouselDecoration) + override fun bind( + viewBinding: FeedItemCarouselBinding, + position: Int, + payloads: MutableList + ) { + if (payloads.contains(PAYLOAD_UPDATE_LIST_VIEW_MODE)) { + updateViewMode(viewBinding) + return } - return viewHolder + super.bind(viewBinding, position, payloads) } override fun bind(viewBinding: FeedItemCarouselBinding, position: Int) { viewBinding.recyclerView.apply { adapter = carouselAdapter } - linearLayoutManager?.onRestoreInstanceState(listState) + carouselLayoutManager?.onRestoreInstanceState(listState) } override fun unbind(viewHolder: GroupieViewHolder) { super.unbind(viewHolder) + listState = carouselLayoutManager?.onSaveInstanceState() + } - listState = linearLayoutManager?.onSaveInstanceState() + private fun updateViewMode(viewBinding: FeedItemCarouselBinding) { + viewBinding.recyclerView.apply { adapter = carouselAdapter } + + val context = viewBinding.root.context + carouselLayoutManager = if (listViewMode) { + LinearLayoutManager(context) + } else { + GridLayoutManager(context, getGridSpanCount(context, DeviceUtils.dpToPx(112, context))) + } + + viewBinding.recyclerView.apply { + layoutManager = carouselLayoutManager + adapter = carouselAdapter + } } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/GroupsHeader.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/GroupsHeader.kt new file mode 100644 index 000000000..8d5088890 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/GroupsHeader.kt @@ -0,0 +1,50 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View +import androidx.core.view.isVisible +import com.xwray.groupie.viewbinding.BindableItem +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.SubscriptionGroupsHeaderBinding + +class GroupsHeader( + private val title: String, + private val onSortClicked: () -> Unit, + private val onToggleListViewModeClicked: () -> Unit, + var showSortButton: Boolean = true, + var listViewMode: Boolean = true +) : BindableItem() { + companion object { + const val PAYLOAD_UPDATE_ICONS = 1 + } + + override fun getLayout(): Int = R.layout.subscription_groups_header + + override fun bind( + viewBinding: SubscriptionGroupsHeaderBinding, + position: Int, + payloads: MutableList + ) { + if (payloads.contains(PAYLOAD_UPDATE_ICONS)) { + updateIcons(viewBinding) + return + } + + super.bind(viewBinding, position, payloads) + } + + override fun bind(viewBinding: SubscriptionGroupsHeaderBinding, position: Int) { + viewBinding.headerTitle.text = title + viewBinding.headerSort.setOnClickListener { onSortClicked() } + viewBinding.headerToggleViewMode.setOnClickListener { onToggleListViewModeClicked() } + updateIcons(viewBinding) + } + + override fun initializeViewBinding(view: View) = SubscriptionGroupsHeaderBinding.bind(view) + + private fun updateIcons(viewBinding: SubscriptionGroupsHeaderBinding) { + viewBinding.headerToggleViewMode.setImageResource( + if (listViewMode) R.drawable.ic_apps else R.drawable.ic_list + ) + viewBinding.headerSort.isVisible = showSortButton + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/Header.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/Header.kt new file mode 100644 index 000000000..87a3ac768 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/Header.kt @@ -0,0 +1,17 @@ +package org.schabi.newpipe.local.subscription.item + +import android.view.View +import com.xwray.groupie.viewbinding.BindableItem +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.SubscriptionHeaderBinding + +class Header(private val title: String) : BindableItem() { + + override fun getLayout(): Int = R.layout.subscription_header + + override fun bind(viewBinding: SubscriptionHeaderBinding, position: Int) { + viewBinding.root.text = title + } + + override fun initializeViewBinding(view: View) = SubscriptionHeaderBinding.bind(view) +} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt deleted file mode 100644 index 79a272178..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.schabi.newpipe.local.subscription.item - -import android.view.View -import android.view.View.OnClickListener -import androidx.annotation.DrawableRes -import androidx.core.view.isVisible -import com.xwray.groupie.viewbinding.BindableItem -import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.HeaderWithMenuItemBinding - -class HeaderWithMenuItem( - val title: String, - @DrawableRes val itemIcon: Int = 0, - var showMenuItem: Boolean = true, - private val onClickListener: (() -> Unit)? = null, - private val menuItemOnClickListener: (() -> Unit)? = null -) : BindableItem() { - companion object { - const val PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM = 1 - } - - override fun getLayout(): Int = R.layout.header_with_menu_item - - override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int, payloads: MutableList) { - if (payloads.contains(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM)) { - updateMenuItemVisibility(viewBinding) - return - } - - super.bind(viewBinding, position, payloads) - } - - override fun bind(viewBinding: HeaderWithMenuItemBinding, position: Int) { - viewBinding.headerTitle.text = title - viewBinding.headerMenuItem.setImageResource(itemIcon) - - val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } } - viewBinding.root.setOnClickListener(listener) - - val menuItemListener = menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } } - viewBinding.headerMenuItem.setOnClickListener(menuItemListener) - updateMenuItemVisibility(viewBinding) - } - - override fun initializeViewBinding(view: View) = HeaderWithMenuItemBinding.bind(view) - - private fun updateMenuItemVisibility(viewBinding: HeaderWithMenuItemBinding) { - viewBinding.headerMenuItem.isVisible = showMenuItem - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index c18a7f487..94de7fef3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -212,7 +212,6 @@ public final class PlayQueueActivity extends AppCompatActivity if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) { unbind(); - finish(); } else { onQueueUpdate(player.getPlayQueue()); buildComponents(); diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt index 095b3ccdb..a6dba0dd5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt @@ -7,6 +7,7 @@ import android.view.View.OnTouchListener import android.widget.ProgressBar import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources +import androidx.core.math.MathUtils import androidx.core.view.isVisible import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R @@ -18,8 +19,6 @@ import org.schabi.newpipe.player.helper.PlayerHelper import org.schabi.newpipe.player.ui.MainPlayerUi import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min /** * GestureListener for the player @@ -114,7 +113,7 @@ class MainPlayerGestureListener( // Update progress bar val oldBrightness = layoutParams.screenBrightness - bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt() + bar.progress = (bar.max * MathUtils.clamp(oldBrightness, 0f, 1f)).toInt() bar.incrementProgressBy(distanceY.toInt()) // Update brightness diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java index 81dc954d1..248104738 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.player.ui; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.player.Player.STATE_COMPLETED; import static org.schabi.newpipe.player.Player.STATE_PAUSED; @@ -52,6 +53,7 @@ import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.detail.VideoDetailFragment; import org.schabi.newpipe.info_list.StreamSegmentAdapter; +import org.schabi.newpipe.info_list.StreamSegmentItem; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.Player; @@ -60,6 +62,7 @@ import org.schabi.newpipe.player.gesture.BasePlayerGestureListener; import org.schabi.newpipe.player.gesture.MainPlayerGestureListener; import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlayerHelper; +import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueAdapter; import org.schabi.newpipe.player.playqueue.PlayQueueItem; @@ -69,6 +72,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; +import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.List; import java.util.Objects; @@ -644,7 +648,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh private void buildSegments() { binding.itemsList.setAdapter(segmentAdapter); binding.itemsList.setClickable(true); - binding.itemsList.setLongClickable(false); + binding.itemsList.setLongClickable(true); binding.itemsList.clearOnScrollListeners(); if (itemTouchHelper != null) { @@ -696,10 +700,30 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh } private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { - return (item, seconds) -> { - segmentAdapter.selectSegment(item); - player.seekTo(seconds * 1000L); - player.triggerProgressUpdate(); + return new StreamSegmentAdapter.StreamSegmentListener() { + @Override + public void onItemClick(@NonNull final StreamSegmentItem item, final int seconds) { + segmentAdapter.selectSegment(item); + player.seekTo(seconds * 1000L); + player.triggerProgressUpdate(); + } + + @Override + public void onItemLongClick(@NonNull final StreamSegmentItem item, final int seconds) { + @Nullable final MediaItemTag currentMetadata = player.getCurrentMetadata(); + if (currentMetadata == null + || currentMetadata.getServiceId() != YouTube.getServiceId()) { + return; + } + + final PlayQueueItem currentItem = player.getCurrentItem(); + if (currentItem != null) { + String videoUrl = player.getVideoUrl(); + videoUrl += ("&t=" + seconds); + ShareUtils.shareText(context, currentItem.getTitle(), + videoUrl, currentItem.getThumbnailUrl()); + } + } }; } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java index aa36a6a5a..90c24c0c6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java @@ -291,7 +291,7 @@ public final class PopupPlayerUi extends VideoPlayerUi { } final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width); - final int actualWidth = Math.min((int) Math.max(width, minimumWidth), screenWidth); + final int actualWidth = MathUtils.clamp(width, (int) minimumWidth, screenWidth); final int actualHeight = (int) getMinimumVideoHeight(width); if (DEBUG) { Log.d(TAG, "updatePopupSize() updated values:" diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java index 1770685e4..3e92f297e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java @@ -5,7 +5,7 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; +import android.content.res.ColorStateList; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -21,7 +21,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.TextViewCompat; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; @@ -214,17 +214,13 @@ public class NotificationActionsPreference extends Preference { .getRoot(); // if present set action icon with correct color - if (NotificationConstants.ACTION_ICONS[action] != 0) { - Drawable drawable = AppCompatResources.getDrawable(getContext(), - NotificationConstants.ACTION_ICONS[action]); - if (drawable != null) { - final int color = ThemeHelper.resolveColorFromAttr(getContext(), - android.R.attr.textColorPrimary); - drawable = DrawableCompat.wrap(drawable).mutate(); - drawable.setTint(color); - radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, - null, drawable, null); - } + final int iconId = NotificationConstants.ACTION_ICONS[action]; + if (iconId != 0) { + radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, iconId, 0); + + final var color = ColorStateList.valueOf(ThemeHelper + .resolveColorFromAttr(getContext(), android.R.attr.textColorPrimary)); + TextViewCompat.setCompoundDrawableTintList(radioButton, color); } radioButton.setText(NotificationConstants.getActionName(getContext(), action)); diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java index ea45c68d2..68b0010c4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceFuzzySearchFunction.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings.preferencesearch; import android.text.TextUtils; +import android.util.Pair; import org.apache.commons.text.similarity.FuzzyScore; @@ -8,6 +9,7 @@ import java.util.Comparator; import java.util.Locale; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; public class PreferenceFuzzySearchFunction @@ -72,39 +74,22 @@ public class PreferenceFuzzySearchFunction ); private final PreferenceSearchItem item; - private final float score; + private final double score; - FuzzySearchSpecificDTO( - final PreferenceSearchItem item, - final String keyword) { + FuzzySearchSpecificDTO(final PreferenceSearchItem item, final String keyword) { this.item = item; - - float attributeScoreSum = 0; - int countOfAttributesWithScore = 0; - for (final Map.Entry, Float> we - : WEIGHT_MAP.entrySet()) { - final String valueToProcess = we.getKey().apply(item); - if (valueToProcess.isEmpty()) { - continue; - } - - attributeScoreSum += - FUZZY_SCORE.fuzzyScore(valueToProcess, keyword) * we.getValue(); - countOfAttributesWithScore++; - } - - if (countOfAttributesWithScore != 0) { - this.score = attributeScoreSum / countOfAttributesWithScore; - } else { - this.score = 0; - } + this.score = WEIGHT_MAP.entrySet().stream() + .map(entry -> new Pair<>(entry.getKey().apply(item), entry.getValue())) + .filter(pair -> !pair.first.isEmpty()) + .collect(Collectors.averagingDouble(pair -> + FUZZY_SCORE.fuzzyScore(pair.first, keyword) * pair.second)); } public PreferenceSearchItem getItem() { return item; } - public float getScore() { + public double getScore() { return score; } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 3b2c52691..13413e89d 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -639,6 +639,11 @@ public final class NavigationHelper { return intent; } + public static void openPlayQueue(final Context context) { + final Intent intent = new Intent(context, PlayQueueActivity.class); + context.startActivity(intent); + } + /*////////////////////////////////////////////////////////////////////////// // Link handling //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt index 0c66cc6d4..5a54b29d2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -100,13 +100,11 @@ object ReleaseVersionUtil { * @return Epoch second of expiry date time */ fun coerceUpdateCheckExpiry(expiryString: String?): Long { - val now = ZonedDateTime.now() - return expiryString?.let { - var expiry = - ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(expiryString)) - expiry = maxOf(expiry, now.plusHours(6)) - expiry = minOf(expiry, now.plusHours(72)) - expiry.toEpochSecond() - } ?: now.plusHours(6).toEpochSecond() + val nowPlus6Hours = ZonedDateTime.now().plusHours(6) + val expiry = expiryString?.let { + ZonedDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(it)) + .coerceIn(nowPlus6Hours, nowPlus6Hours.plusHours(66)) + } ?: nowPlus6Hours + return expiry.toEpochSecond() } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 389af80ee..ea22e9368 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -349,7 +349,7 @@ public final class ThemeHelper { return false; } else if (listMode.equals(context.getString(R.string.list_view_mode_grid_key))) { return true; - } else { + } else /* listMode.equals("auto") */ { final Configuration configuration = context.getResources().getConfiguration(); return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 343b13ef8..808928370 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -1,5 +1,25 @@ package us.shandian.giga.ui.adapter; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; +import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; +import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; +import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; +import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE; +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; +import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; +import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; +import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; +import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; +import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; + import android.annotation.SuppressLint; import android.app.NotificationManager; import android.content.Context; @@ -10,7 +30,6 @@ import android.os.Build; import android.os.Handler; import android.os.Message; import android.util.Log; -import android.util.SparseArray; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; @@ -38,10 +57,11 @@ import com.google.android.material.snackbar.Snackbar; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.error.ErrorUtil; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.ShareUtils; @@ -60,47 +80,19 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.MissionRecoveryInfo; -import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; import us.shandian.giga.ui.common.ProgressDrawable; import us.shandian.giga.util.Utility; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; -import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; -import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; -import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; -import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; -import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE; -import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; -import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; -import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; -import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; -import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; -import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; -import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT; -import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; -import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; - public class MissionAdapter extends Adapter implements Handler.Callback { - private static final SparseArray ALGORITHMS = new SparseArray<>(); private static final String TAG = "MissionAdapter"; private static final String UNDEFINED_PROGRESS = "--.-%"; private static final String DEFAULT_MIME_TYPE = "*/*"; private static final String UNDEFINED_ETA = "--:--"; - private static final int HASH_NOTIFICATION_ID = 123790; - static { - ALGORITHMS.put(R.id.md5, "MD5"); - ALGORITHMS.put(R.id.sha1, "SHA1"); - } - private final Context mContext; private final LayoutInflater mInflater; private final DownloadManager mDownloadManager; @@ -697,7 +689,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb .build()); final StoredFileHelper storage = h.item.mission.storage; compositeDisposable.add( - Observable.fromCallable(() -> Utility.checksum(storage, ALGORITHMS.get(id))) + Observable.fromCallable(() -> Utility.checksum(storage, id)) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 9e6787d5d..4cd424ab9 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -13,8 +13,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import com.google.android.exoplayer2.util.Util; + import org.schabi.newpipe.R; -import org.schabi.newpipe.streams.io.SharpStream; +import org.schabi.newpipe.streams.io.SharpInputStream; +import org.schabi.newpipe.streams.io.StoredFileHelper; import java.io.BufferedOutputStream; import java.io.File; @@ -25,11 +28,9 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.HttpURLConnection; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Locale; -import org.schabi.newpipe.streams.io.StoredFileHelper; +import okio.ByteString; public class Utility { @@ -203,44 +204,18 @@ public class Utility { Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } - public static String checksum(StoredFileHelper source, String algorithm) { - MessageDigest md; - - try { - md = MessageDigest.getInstance(algorithm); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); + public static String checksum(final StoredFileHelper source, final int algorithmId) + throws IOException { + ByteString byteString; + try (var inputStream = new SharpInputStream(source.getStream())) { + byteString = ByteString.of(Util.toByteArray(inputStream)); } - - SharpStream i; - - try { - i = source.getStream(); - } catch (Exception e) { - throw new RuntimeException(e); + if (algorithmId == R.id.md5) { + byteString = byteString.md5(); + } else if (algorithmId == R.id.sha1) { + byteString = byteString.sha1(); } - - byte[] buf = new byte[1024]; - int len; - - try { - while ((len = i.read(buf)) != -1) { - md.update(buf, 0, len); - } - } catch (IOException e) { - // nothing to do - } - - byte[] digest = md.digest(); - - // HEX - StringBuilder sb = new StringBuilder(); - for (byte b : digest) { - sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); - } - - return sb.toString(); - + return byteString.hex(); } @SuppressWarnings("ResultOfMethodCallIgnored") diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 5904724ad..a29fa8c78 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -698,6 +698,16 @@ android:paddingRight="@dimen/video_item_search_padding" tools:ignore="RtlHardcoded"> + + +