Merge pull request #9290 from TeamNewPipe/release/0.24.1

Release v0.24.1
master v0.24.1
Tobi 3 months ago committed by GitHub
commit a95a5ed13e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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:

@ -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

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".DebugApp"

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
@ -14,6 +13,9 @@
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<application
android:name=".App"
@ -151,6 +153,7 @@
<data android:pathPrefix="/channel/" />
<data android:pathPrefix="/user/" />
<data android:pathPrefix="/c/" />
<data android:pathPrefix="/@" />
<!-- playlist prefix -->
<data android:pathPrefix="/playlist" />
</intent-filter>

@ -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
}
}

@ -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}

@ -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

@ -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()

@ -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);
}

@ -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<I, N> extends BaseStateFragment<I>
}
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);
}
}

@ -61,5 +61,6 @@ class StreamSegmentAdapter(
interface StreamSegmentListener {
fun onItemClick(item: StreamSegmentItem, seconds: Int)
fun onItemLongClick(item: StreamSegmentItem, seconds: Int)
}
}

@ -41,6 +41,7 @@ class StreamSegmentItem(
viewHolder.root.findViewById<TextView>(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
}

@ -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");
}
})
),

@ -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;
}
});
}
}

@ -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<FeedState>() {
// 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) {

@ -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<List<PlaylistSt
///////////////////////////////////////////////////////////////////////////
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
return new Subscriber<List<PlaylistStreamEntry>>() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
showLoading();
@ -395,31 +394,21 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
isRemovingWatched = true;
showLoading();
disposables.add(playlistManager.getPlaylistStreams(playlistId)
.subscribeOn(Schedulers.io())
.map((List<PlaylistStreamEntry> playlist) -> {
// Playlist data
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
// History data
final HistoryRecordManager recordManager =
new HistoryRecordManager(getContext());
final Iterator<StreamHistoryEntry> 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<PlaylistStreamEntry> notWatchedItems = new ArrayList<>();
boolean thumbnailVideoRemoved = false;
// already sorted by ^ getStreamHistorySortedById(), binary search can be used
final ArrayList<Long> 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<List<PlaylistSt
}
}
} else {
final Iterator<StreamStateEntity> 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<List<PlaylistSt
}
}
return Flowable.just(notWatchedItems, thumbnailVideoRemoved);
})
return new Pair<>(notWatchedItems, thumbnailVideoRemoved);
});
disposables.add(streamsMaybe.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(flow -> {
final List<PlaylistStreamEntry> notWatchedItems =
(List<PlaylistStreamEntry>) flow.blockingFirst();
final boolean thumbnailVideoRemoved = (Boolean) flow.blockingLast();
final List<PlaylistStreamEntry> 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<List<PlaylistSt
}
setVideoCount(itemListAdapter.getItemsList().size());
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
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<List<PlaylistSt
hideLoading();
}
private void showHoldToAppendTipIfNeeded() {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////

@ -22,13 +22,12 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
@ -43,11 +42,13 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
@ -74,9 +75,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private lateinit var carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
private lateinit var feedGroupsCarousel: FeedGroupCarouselItem
private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section()
private val requestExportLauncher =
@ -90,7 +91,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
@State
@JvmField
var feedGroupsListState: Parcelable? = null
var feedGroupsCarouselState: Parcelable? = null
init {
setHasOptionsMenu(true)
@ -100,11 +101,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// 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<SubscriptionState>() {
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<SubscriptionState>() {
menuItem: MenuItem,
onClick: Runnable
): MenuItem {
menuItem.setOnMenuItemClickListener { _ ->
menuItem.setOnMenuItemClickListener {
onClick.run()
true
}
@ -245,35 +241,74 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// Fragment Views
// ////////////////////////////////////////////////////////////////////////
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountChannels(context) else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
binding.itemsList.itemAnimator = null
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
it?.let { (groups, listViewMode) ->
handleFeedGroups(groups, listViewMode)
}
}
setupInitialLayout()
}
private fun setupInitialLayout() {
Section().apply {
val carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
carouselAdapter.add(feedGroupsSection)
carouselAdapter.add(FeedGroupAddItem())
carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.setOnItemClickListener { item, _ ->
listenerFeedGroups.selected(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) {
if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
return@setOnItemLongClickListener false
}
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)
}
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
feedGroupsCarousel = FeedGroupCarouselItem(
carouselAdapter = carouselAdapter,
listViewMode = viewModel.getListViewMode()
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
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)
}
@ -282,27 +317,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
groupAdapter.add(
Section(
HeaderWithMenuItem(
getString(R.string.tab_subscriptions)
),
Header(getString(R.string.tab_subscriptions)),
listOf(subscriptionsSection)
)
)
}
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountChannels(context) else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
private fun toggleListViewMode() {
viewModel.setListViewMode(!viewModel.getListViewMode())
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@ -346,21 +368,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit
private val listenerFeedGroups = object : OnClickGesture<Item<*>> {
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<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
@ -402,16 +409,38 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
}
private fun handleFeedGroups(groups: List<Group>) {
feedGroupsSection.update(groups)
if (feedGroupsListState != null) {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
feedGroupsListState = null
private fun handleFeedGroups(groups: List<Group>, 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)
}
}
}
// /////////////////////////////////////////////////////////////////////////

@ -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)
// true -> list view, false -> grid view
private val listViewMode = BehaviorProcessor.createDefault(
!ThemeHelper.shouldUseGridLayout(application)
)
private val listViewModeFlowable = listViewMode.distinctUntilChanged()
private val mutableStateLiveData = MutableLiveData<SubscriptionState>()
private val mutableFeedGroupsLiveData = MutableLiveData<List<Group>>()
private val mutableFeedGroupsLiveData = MutableLiveData<Pair<List<Group>, Boolean>>()
val stateLiveData: LiveData<SubscriptionState> = mutableStateLiveData
val feedGroupsLiveData: LiveData<List<Group>> = mutableFeedGroupsLiveData
val feedGroupsLiveData: LiveData<Pair<List<Group>, Boolean>> = mutableFeedGroupsLiveData
private var feedGroupItemsDisposable = feedDatabaseManager.groups()
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<Group>) : SubscriptionState()
data class ErrorState(val error: Throwable? = null) : SubscriptionState()

@ -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
}
}
}

@ -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) {

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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
)
}
}
}
}

@ -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<FeedGroupAddNewGridItemBinding>() {
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
}
}

@ -5,8 +5,10 @@ import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FeedGroupAddNewItemBinding
class FeedGroupAddItem : BindableItem<FeedGroupAddNewItemBinding>() {
class FeedGroupAddNewItem : BindableItem<FeedGroupAddNewItemBinding>() {
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
}
}

@ -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<FeedGroupCardGridItemBinding>() {
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)
}

@ -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<GroupieViewHolder<FeedItemCarouselBinding>>
private val carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>,
var listViewMode: Boolean
) : BindableItem<FeedItemCarouselBinding>() {
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)
linearLayoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
val viewBinding = FeedItemCarouselBinding.bind(view)
updateViewMode(viewBinding)
return viewBinding