Merge pull request #3371 from mauriciocolli/feed-hide-played-items
Add ability to hide played items in a feed
This commit is contained in:
commit
be676ad93c
31 changed files with 745 additions and 217 deletions
|
|
@ -9,7 +9,8 @@ import androidx.room.Update
|
|||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
|
@ -20,21 +21,34 @@ abstract class FeedDAO {
|
|||
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
|
@ -42,16 +56,88 @@ abstract class FeedDAO {
|
|||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
|
||||
INNER JOIN feed_group fg
|
||||
ON fg.uid = fgs.group_id
|
||||
|
||||
WHERE fgs.group_id = :groupId
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
* @return all of the non-live, never-played and non-finished streams in the feed
|
||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE (
|
||||
sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
|
||||
|
||||
/**
|
||||
* @see StreamStateEntity.isFinished()
|
||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
* @param groupId the group id to get streams of
|
||||
* @return all of the non-live, never-played and non-finished streams for the given feed group
|
||||
* (all of the cited conditions must hold for a stream to be in the returned list)
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.*, sst.progress_time
|
||||
FROM streams s
|
||||
|
||||
LEFT JOIN stream_state sst
|
||||
ON s.uid = sst.stream_id
|
||||
|
||||
LEFT JOIN stream_history sh
|
||||
ON s.uid = sh.stream_id
|
||||
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = f.subscription_id
|
||||
|
||||
WHERE fgs.group_id = :groupId
|
||||
AND (
|
||||
sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
"""
|
||||
)
|
||||
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WA
|
|||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
|
|
@ -80,7 +80,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
|||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ data class PlaylistStreamEntry(
|
|||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||
val progressMillis: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
|
|||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
|
|
@ -64,7 +64,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
|||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.room.Embedded
|
|||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
|
|
@ -13,8 +13,8 @@ class StreamStatisticsEntry(
|
|||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
@ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||
val progressMillis: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package org.schabi.newpipe.database.stream
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
|
||||
data class StreamWithState(
|
||||
@Embedded
|
||||
val stream: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
|
||||
val stateProgressMillis: Long?
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import androidx.room.ColumnInfo;
|
|||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Objects;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
|
|
@ -25,26 +25,31 @@ public class StreamStateEntity {
|
|||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
public static final String STREAM_PROGRESS_MILLIS = "progress_time";
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold.
|
||||
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
|
||||
|
||||
/**
|
||||
* Playback state will not be saved, if time left is less than this threshold.
|
||||
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||
* (60000ms = 60s).
|
||||
* @see #isFinished(long)
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||
public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||
private long progressTime;
|
||||
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
|
||||
private long progressMillis;
|
||||
|
||||
public StreamStateEntity(final long streamUid, final long progressTime) {
|
||||
public StreamStateEntity(final long streamUid, final long progressMillis) {
|
||||
this.streamUid = streamUid;
|
||||
this.progressTime = progressTime;
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
|
|
@ -55,27 +60,53 @@ public class StreamStateEntity {
|
|||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public long getProgressTime() {
|
||||
return progressTime;
|
||||
public long getProgressMillis() {
|
||||
return progressMillis;
|
||||
}
|
||||
|
||||
public void setProgressTime(final long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
public void setProgressMillis(final long progressMillis) {
|
||||
this.progressMillis = progressMillis;
|
||||
}
|
||||
|
||||
public boolean isValid(final int durationInSeconds) {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||
/**
|
||||
* The state will be considered valid, and thus be saved, if the progress is more than {@link
|
||||
* #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether this stream state entity should be saved or not
|
||||
*/
|
||||
public boolean isValid(final long durationInSeconds) {
|
||||
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
|| progressMillis > durationInSeconds * 1000 / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* The video will be considered as finished, if the time left is less than {@link
|
||||
* #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
|
||||
* The state will be saved anyway, so that it can be shown under stream info items, but the
|
||||
* player will not resume if a state is considered as finished. Finished streams are also the
|
||||
* ones that can be filtered out in the feed fragment.
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
|
||||
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
|
||||
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||
* @return whether the stream is finished or not
|
||||
*/
|
||||
public boolean isFinished(final long durationInSeconds) {
|
||||
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
&& progressMillis >= durationInSeconds * 1000 * 3 / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable final Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||
&& ((StreamStateEntity) obj).progressMillis == progressMillis;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(streamUid, progressMillis);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
|||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
inflater.inflate(R.menu.menu_main_fragment, menu);
|
||||
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
|
|
|
|||
|
|
@ -1669,7 +1669,7 @@ public final class VideoDetailFragment
|
|||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(state -> {
|
||||
showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000);
|
||||
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
|
||||
animate(binding.positionView, true, 500);
|
||||
animate(binding.detailPositionView, true, 500);
|
||||
}, e -> {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state2.getProgressTime()));
|
||||
.toSeconds(state2.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
@ -121,10 +121,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||
itemProgressView.setMax((int) item.getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(state.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(state.getProgressTime()));
|
||||
.toSeconds(state.getProgressMillis()));
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import org.schabi.newpipe.NewPipeDatabase
|
|||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
|
|
@ -38,16 +39,19 @@ class FeedDatabaseManager(context: Context) {
|
|||
|
||||
fun database() = database
|
||||
|
||||
fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<List<StreamInfoItem>> {
|
||||
val streams = when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams()
|
||||
else -> feedTable.getAllStreamsFromGroup(groupId)
|
||||
}
|
||||
|
||||
return streams.map {
|
||||
val items = ArrayList<StreamInfoItem>(it.size)
|
||||
it.mapTo(items) { stream -> stream.toStreamInfoItem() }
|
||||
return@map items
|
||||
fun getStreams(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
getPlayedStreams: Boolean = true
|
||||
): Flowable<List<StreamWithState>> {
|
||||
return when (groupId) {
|
||||
FeedGroupEntity.GROUP_ALL_ID -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreams()
|
||||
else feedTable.getLiveOrNotPlayedStreams()
|
||||
}
|
||||
else -> {
|
||||
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
|
||||
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +64,10 @@ class FeedDatabaseManager(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
|
||||
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
fun outdatedSubscriptionsForGroup(
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
outdatedThreshold: OffsetDateTime
|
||||
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
|
||||
|
||||
fun markAsOutdated(subscriptionId: Long) = feedTable
|
||||
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
|
||||
|
|
@ -93,10 +99,7 @@ class FeedDatabaseManager(context: Context) {
|
|||
}
|
||||
|
||||
feedTable.setLastUpdatedForSubscription(
|
||||
FeedLastUpdatedEntity(
|
||||
subscriptionId,
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
)
|
||||
FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +111,12 @@ class FeedDatabaseManager(context: Context) {
|
|||
fun clear() {
|
||||
feedTable.deleteAll()
|
||||
val deletedOrphans = streamTable.deleteOrphans()
|
||||
if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans")
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
this::class.java.simpleName,
|
||||
"clear() → streamTable.deleteOrphans() → $deletedOrphans"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -122,7 +130,8 @@ class FeedDatabaseManager(context: Context) {
|
|||
}
|
||||
|
||||
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
|
||||
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
|
||||
return Completable
|
||||
.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@
|
|||
|
||||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
|
|
@ -30,11 +34,18 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieViewHolder
|
||||
import com.xwray.groupie.Item
|
||||
import com.xwray.groupie.OnItemClickListener
|
||||
import com.xwray.groupie.OnItemLongClickListener
|
||||
import icepick.State
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
|
@ -49,33 +60,46 @@ import org.schabi.newpipe.error.ErrorInfo
|
|||
import org.schabi.newpipe.error.UserAction
|
||||
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog
|
||||
import org.schabi.newpipe.ktx.animate
|
||||
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import org.schabi.newpipe.util.NavigationHelper
|
||||
import org.schabi.newpipe.util.StreamDialogEntry
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.ArrayList
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
||||
class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var _feedBinding: FragmentFeedBinding? = null
|
||||
private val feedBinding get() = _feedBinding!!
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private lateinit var viewModel: FeedViewModel
|
||||
@State
|
||||
@JvmField
|
||||
var listState: Parcelable? = null
|
||||
@State @JvmField var listState: Parcelable? = null
|
||||
|
||||
private var groupId = FeedGroupEntity.GROUP_ALL_ID
|
||||
private var groupName = ""
|
||||
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
||||
|
||||
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
|
||||
@State @JvmField var showPlayedItems: Boolean = true
|
||||
|
||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||
private var updateListViewModeOnResume = false
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
setUseDefaultStateSaving(false)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -84,6 +108,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
||||
?: FeedGroupEntity.GROUP_ALL_ID
|
||||
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
|
||||
|
||||
onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||
updateListViewModeOnResume = true
|
||||
}
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.registerOnSharedPreferenceChangeListener(onSettingsChangeListener)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
|
@ -95,8 +127,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
_feedBinding = FragmentFeedBinding.bind(rootView)
|
||||
super.onViewCreated(rootView, savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||
val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
|
||||
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
|
||||
|
||||
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
|
||||
setOnItemClickListener(listenerStreamItem)
|
||||
setOnItemLongClickListener(listenerStreamItem)
|
||||
}
|
||||
|
||||
feedBinding.itemsList.adapter = groupAdapter
|
||||
setupListViewMode()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
@ -107,6 +148,23 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateRelativeTimeViews()
|
||||
|
||||
if (updateListViewModeOnResume) {
|
||||
updateListViewModeOnResume = false
|
||||
|
||||
setupListViewMode()
|
||||
if (viewModel.stateLiveData.value != null) {
|
||||
handleResult(viewModel.stateLiveData.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setupListViewMode() {
|
||||
// does everything needed to setup the layouts for grid or list modes
|
||||
groupAdapter.spanCount = if (shouldUseGridLayout()) getGridSpanCount() else 1
|
||||
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
||||
spanSizeLookup = groupAdapter.spanSizeLookup
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
|
|
@ -129,21 +187,21 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||
activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
|
||||
activity.supportActionBar?.subtitle = groupName
|
||||
|
||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||
|
||||
if (useAsFrontPage) {
|
||||
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||
}
|
||||
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_item_feed_help) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
||||
val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
val usingDedicatedMethod = sharedPreferences
|
||||
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
||||
val enableDisableButtonText = when {
|
||||
usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
|
||||
else -> R.string.feed_use_dedicated_fetch_method_enable_button
|
||||
|
|
@ -160,6 +218,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
.create()
|
||||
.show()
|
||||
return true
|
||||
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
|
||||
showPlayedItems = !item.isChecked
|
||||
updateTogglePlayedItemsButton(item)
|
||||
viewModel.togglePlayedItems(showPlayedItems)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
|
@ -172,18 +234,33 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
override fun onDestroy() {
|
||||
disposables.dispose()
|
||||
if (onSettingsChangeListener != null) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(onSettingsChangeListener)
|
||||
onSettingsChangeListener = null
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
feedBinding.itemsList.adapter = null
|
||||
_feedBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
|
||||
menuItem.isChecked = showPlayedItems
|
||||
menuItem.icon = AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
|
||||
)
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////
|
||||
// Handling
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun showLoading() {
|
||||
super.showLoading()
|
||||
|
|
@ -195,6 +272,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
override fun hideLoading() {
|
||||
super.hideLoading()
|
||||
feedBinding.itemsList.animate(true, 0)
|
||||
feedBinding.refreshRootView.animate(true, 200)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
feedBinding.swipeRefreshLayout.isRefreshing = false
|
||||
|
|
@ -220,7 +298,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
override fun handleError() {
|
||||
super.handleError()
|
||||
infoListAdapter.clearStreamItemList()
|
||||
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
||||
feedBinding.refreshRootView.animate(false, 0)
|
||||
feedBinding.loadingProgressText.animate(false, 0)
|
||||
|
|
@ -248,8 +325,74 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
feedBinding.loadingProgressBar.max = progressState.maxProgress
|
||||
}
|
||||
|
||||
private fun showStreamDialog(item: StreamInfoItem) {
|
||||
val context = context
|
||||
val activity: Activity? = getActivity()
|
||||
if (context == null || context.resources == null || activity == null) return
|
||||
|
||||
val entries = ArrayList<StreamDialogEntry>()
|
||||
if (PlayerHolder.getType() != null) {
|
||||
entries.add(StreamDialogEntry.enqueue)
|
||||
}
|
||||
if (item.streamType == StreamType.AUDIO_STREAM) {
|
||||
entries.addAll(
|
||||
listOf(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share,
|
||||
StreamDialogEntry.open_in_browser
|
||||
)
|
||||
)
|
||||
} else {
|
||||
entries.addAll(
|
||||
listOf(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share,
|
||||
StreamDialogEntry.open_in_browser
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
StreamDialogEntry.setEnabledEntries(entries)
|
||||
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
|
||||
StreamDialogEntry.clickOn(which, this, item)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
|
||||
override fun onItemClick(item: Item<*>, view: View) {
|
||||
if (item is StreamItem) {
|
||||
val stream = item.streamWithState.stream
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
requireContext(), fm,
|
||||
stream.serviceId, stream.url, stream.title, null, false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
|
||||
if (item is StreamItem) {
|
||||
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
|
||||
infoListAdapter.setInfoItemList(loadedState.items)
|
||||
|
||||
val itemVersion = if (shouldUseGridLayout()) {
|
||||
StreamItem.ItemVersion.GRID
|
||||
} else {
|
||||
StreamItem.ItemVersion.NORMAL
|
||||
}
|
||||
loadedState.items.forEach { it.itemVersion = itemVersion }
|
||||
|
||||
groupAdapter.updateAsync(loadedState.items, false, null)
|
||||
|
||||
listState?.run {
|
||||
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
||||
listState = null
|
||||
|
|
@ -290,7 +433,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
}
|
||||
|
||||
private fun handleItemsErrors(errors: List<Throwable>) {
|
||||
errors.forEachIndexed() { i, t ->
|
||||
errors.forEachIndexed { i, t ->
|
||||
if (t is FeedLoadService.RequestException &&
|
||||
t.cause is ContentNotAvailableException
|
||||
) {
|
||||
|
|
@ -357,7 +500,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
|
||||
private fun updateRelativeTimeViews() {
|
||||
updateRefreshViewState()
|
||||
infoListAdapter.notifyDataSetChanged()
|
||||
groupAdapter.notifyItemRangeChanged(
|
||||
0, groupAdapter.itemCount,
|
||||
StreamItem.UPDATE_RELATIVE_TIME
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateRefreshViewState() {
|
||||
|
|
@ -372,8 +518,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
override fun doInitialLoadLogic() {}
|
||||
override fun loadMoreItems() {}
|
||||
override fun hasMoreItems() = false
|
||||
|
||||
override fun reloadContent() {
|
||||
getActivity()?.startService(
|
||||
|
|
@ -384,6 +528,35 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|||
listState = null
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
// Grid Mode
|
||||
// /////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: Move these out of this class, as it can be reused
|
||||
|
||||
private fun shouldUseGridLayout(): Boolean {
|
||||
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
||||
|
||||
return when (listMode) {
|
||||
getString(R.string.list_view_mode_auto_key) -> {
|
||||
val configuration = resources.configuration
|
||||
|
||||
(
|
||||
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
||||
)
|
||||
}
|
||||
getString(R.string.list_view_mode_grid_key) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGridSpanCount(): Int {
|
||||
val minWidth = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width)
|
||||
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_GROUP_ID = "ARG_GROUP_ID"
|
||||
const val KEY_GROUP_NAME = "ARG_GROUP_NAME"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package org.schabi.newpipe.local.feed
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
sealed class FeedState {
|
||||
|
|
@ -12,7 +12,7 @@ sealed class FeedState {
|
|||
) : FeedState()
|
||||
|
||||
data class LoadedState(
|
||||
val items: List<StreamInfoItem>,
|
||||
val items: List<StreamItem>,
|
||||
val oldestUpdate: OffsetDateTime? = null,
|
||||
val notLoadedCount: Long,
|
||||
val itemsErrors: List<Throwable> = emptyList()
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function4
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.local.feed.item.StreamItem
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
|
||||
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
|
||||
|
|
@ -20,26 +22,33 @@ import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
|
|||
import java.time.OffsetDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
|
||||
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(context.applicationContext, groupId) as T
|
||||
}
|
||||
}
|
||||
|
||||
class FeedViewModel(
|
||||
applicationContext: Context,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true
|
||||
) : ViewModel() {
|
||||
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val streamItems = toggleShowPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
.switchMap { showPlayedItems ->
|
||||
feedDatabaseManager.getStreams(groupId, showPlayedItems)
|
||||
}
|
||||
|
||||
private val mutableStateLiveData = MutableLiveData<FeedState>()
|
||||
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
|
||||
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
feedDatabaseManager.asStreamItems(groupId),
|
||||
streamItems,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
|
||||
|
||||
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
|
||||
t3: Long, t4: List<OffsetDateTime> ->
|
||||
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
|
||||
}
|
||||
)
|
||||
|
|
@ -49,9 +58,9 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
|
|||
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
|
||||
mutableStateLiveData.postValue(
|
||||
when (event) {
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount)
|
||||
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
|
||||
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
|
||||
is ErrorResultEvent -> FeedState.ErrorState(event.error)
|
||||
}
|
||||
)
|
||||
|
|
@ -66,5 +75,20 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
|
|||
combineDisposable.dispose()
|
||||
}
|
||||
|
||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: OffsetDateTime?)
|
||||
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
|
||||
|
||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
private val showPlayedItems: Boolean
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package org.schabi.newpipe.local.feed.item
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nostra13.universalimageloader.core.ImageLoader
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import org.schabi.newpipe.MainActivity
|
||||
import org.schabi.newpipe.R
|
||||
import org.schabi.newpipe.database.stream.StreamWithState
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.databinding.ListStreamItemBinding
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants
|
||||
import org.schabi.newpipe.util.Localization
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class StreamItem(
|
||||
val streamWithState: StreamWithState,
|
||||
var itemVersion: ItemVersion = ItemVersion.NORMAL
|
||||
) : BindableItem<ListStreamItemBinding>() {
|
||||
companion object {
|
||||
const val UPDATE_RELATIVE_TIME = 1
|
||||
}
|
||||
|
||||
private val stream: StreamEntity = streamWithState.stream
|
||||
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
|
||||
|
||||
override fun getId(): Long = stream.uid
|
||||
|
||||
enum class ItemVersion { NORMAL, MINI, GRID }
|
||||
|
||||
override fun getLayout(): Int = when (itemVersion) {
|
||||
ItemVersion.NORMAL -> R.layout.list_stream_item
|
||||
ItemVersion.MINI -> R.layout.list_stream_mini_item
|
||||
ItemVersion.GRID -> R.layout.list_stream_grid_item
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view)
|
||||
|
||||
override fun bind(viewBinding: ListStreamItemBinding, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.contains(UPDATE_RELATIVE_TIME)) {
|
||||
if (itemVersion != ItemVersion.MINI) {
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
super.bind(viewBinding, position, payloads)
|
||||
}
|
||||
|
||||
override fun bind(viewBinding: ListStreamItemBinding, position: Int) {
|
||||
viewBinding.itemVideoTitleView.text = stream.title
|
||||
viewBinding.itemUploaderView.text = stream.uploader
|
||||
|
||||
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
|
||||
|
||||
if (stream.duration > 0) {
|
||||
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
viewBinding.itemDurationView.context,
|
||||
R.color.duration_background_color
|
||||
)
|
||||
)
|
||||
viewBinding.itemDurationView.visibility = View.VISIBLE
|
||||
|
||||
if (stateProgressTime != null) {
|
||||
viewBinding.itemProgressView.visibility = View.VISIBLE
|
||||
viewBinding.itemProgressView.max = stream.duration.toInt()
|
||||
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt()
|
||||
} else {
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
} else if (isLiveStream) {
|
||||
viewBinding.itemDurationView.setText(R.string.duration_live)
|
||||
viewBinding.itemDurationView.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
viewBinding.itemDurationView.context,
|
||||
R.color.live_duration_background_color
|
||||
)
|
||||
)
|
||||
viewBinding.itemDurationView.visibility = View.VISIBLE
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
} else {
|
||||
viewBinding.itemDurationView.visibility = View.GONE
|
||||
viewBinding.itemProgressView.visibility = View.GONE
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(
|
||||
stream.thumbnailUrl, viewBinding.itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
|
||||
)
|
||||
|
||||
if (itemVersion != ItemVersion.MINI) {
|
||||
viewBinding.itemAdditionalDetails.text =
|
||||
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isLongClickable() = when (stream.streamType) {
|
||||
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun getStreamInfoDetailLine(context: Context): String {
|
||||
var viewsAndDate = ""
|
||||
val viewCount = stream.viewCount
|
||||
if (viewCount != null && viewCount >= 0) {
|
||||
viewsAndDate = when (stream.streamType) {
|
||||
AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount)
|
||||
LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount)
|
||||
else -> Localization.shortViewCount(context, viewCount)
|
||||
}
|
||||
}
|
||||
val uploadDate = getFormattedRelativeUploadDate(context)
|
||||
return when {
|
||||
!TextUtils.isEmpty(uploadDate) -> when {
|
||||
viewsAndDate.isEmpty() -> uploadDate!!
|
||||
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
|
||||
}
|
||||
else -> viewsAndDate
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedRelativeUploadDate(context: Context): String? {
|
||||
val uploadDate = stream.uploadDate
|
||||
return if (uploadDate != null) {
|
||||
var formattedRelativeTime = Localization.relativeTime(uploadDate)
|
||||
|
||||
if (MainActivity.DEBUG) {
|
||||
val key = context.getString(R.string.show_original_time_ago_key)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, false)) {
|
||||
formattedRelativeTime += " (" + stream.textualUploadDate + ")"
|
||||
}
|
||||
}
|
||||
|
||||
formattedRelativeTime
|
||||
} else {
|
||||
stream.textualUploadDate
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSpanSize(spanCount: Int, position: Int): Int {
|
||||
return if (itemVersion == ItemVersion.GRID) 1 else spanCount
|
||||
}
|
||||
}
|
||||
|
|
@ -211,11 +211,11 @@ public class HistoryRecordManager {
|
|||
|
||||
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
|
||||
return queueItem.getStream()
|
||||
.map((info) -> streamTable.upsert(new StreamEntity(info)))
|
||||
.map(info -> streamTable.upsert(new StreamEntity(info)))
|
||||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) queueItem.getDuration()))
|
||||
.filter(state -> state.isValid(queueItem.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
|
|
@ -224,18 +224,16 @@ public class HistoryRecordManager {
|
|||
.flatMapPublisher(streamStateTable::getState)
|
||||
.firstElement()
|
||||
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
|
||||
.filter(state -> state.isValid((int) info.getDuration()))
|
||||
.filter(state -> state.isValid(info.getDuration()))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
|
||||
return Completable.fromAction(() -> database.runInTransaction(() -> {
|
||||
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
|
||||
if (state.isValid((int) info.getDuration())) {
|
||||
final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
|
||||
if (state.isValid(info.getDuration())) {
|
||||
streamStateTable.upsert(state);
|
||||
} else {
|
||||
streamStateTable.deleteState(streamId);
|
||||
}
|
||||
})).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (item.getProgressTime() > 0) {
|
||||
if (item.getProgressMillis() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
@ -109,14 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||
}
|
||||
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
if (item.getProgressTime() > 0) {
|
||||
if (item.getProgressMillis() > 0) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
@ -140,14 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||
}
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||
|
||||
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
|
||||
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
|
||||
if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
} else {
|
||||
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
|
||||
.toSeconds(item.getProgressTime()));
|
||||
.toSeconds(item.getProgressMillis()));
|
||||
ViewUtils.animate(itemProgressView, true, 500);
|
||||
}
|
||||
} else if (itemProgressView.getVisibility() == View.VISIBLE) {
|
||||
|
|
|
|||
|
|
@ -671,7 +671,11 @@ public final class Player implements
|
|||
//.doFinally()
|
||||
.subscribe(
|
||||
state -> {
|
||||
newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime());
|
||||
if (!state.isFinished(newQueue.getItem().getDuration())) {
|
||||
// resume playback only if the stream was not played to the end
|
||||
newQueue.setRecovery(newQueue.getIndex(),
|
||||
state.getProgressMillis());
|
||||
}
|
||||
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
|
||||
playbackSkipSilence, playWhenReady, isMuted);
|
||||
},
|
||||
|
|
@ -1935,9 +1939,7 @@ public final class Player implements
|
|||
break;
|
||||
case com.google.android.exoplayer2.Player.STATE_ENDED: // 4
|
||||
changeState(STATE_COMPLETED);
|
||||
if (currentMetadata != null) {
|
||||
resetStreamProgressState(currentMetadata.getMetadata());
|
||||
}
|
||||
saveStreamProgressStateCompleted();
|
||||
isPrepared = false;
|
||||
break;
|
||||
}
|
||||
|
|
@ -2398,7 +2400,7 @@ public final class Player implements
|
|||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
case DISCONTINUITY_REASON_INTERNAL:
|
||||
if (playQueue.getIndex() != newWindowIndex) {
|
||||
resetStreamProgressState(playQueue.getItem());
|
||||
saveStreamProgressStateCompleted(); // current stream has ended
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
|
|
@ -2789,61 +2791,47 @@ public final class Player implements
|
|||
}
|
||||
}
|
||||
|
||||
private void saveStreamProgressState(final StreamInfo info, final long progress) {
|
||||
if (info == null) {
|
||||
private void saveStreamProgressState(final long progressMillis) {
|
||||
if (currentMetadata == null
|
||||
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "saveStreamProgressState() called");
|
||||
Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
|
||||
+ ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]");
|
||||
}
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe();
|
||||
databaseUpdateDisposable.add(stateSaver);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetStreamProgressState(final PlayQueueItem queueItem) {
|
||||
if (queueItem == null) {
|
||||
return;
|
||||
}
|
||||
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
|
||||
final Disposable stateSaver = queueItem.getStream()
|
||||
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe();
|
||||
databaseUpdateDisposable.add(stateSaver);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetStreamProgressState(final StreamInfo info) {
|
||||
saveStreamProgressState(info, 0);
|
||||
databaseUpdateDisposable
|
||||
.add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError((e) -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})
|
||||
.onErrorComplete()
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
public void saveStreamProgressState() {
|
||||
if (exoPlayerIsNull() || currentMetadata == null) {
|
||||
if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|
||||
|| playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) {
|
||||
// Make sure play queue and current window index are equal, to prevent saving state for
|
||||
// the wrong stream on discontinuity (e.g. when the stream just changed but the
|
||||
// playQueue index and currentMetadata still haven't updated)
|
||||
return;
|
||||
}
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
if (playQueue != null) {
|
||||
// Save current position. It will help to restore this position once a user
|
||||
// wants to play prev or next stream from the queue
|
||||
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
|
||||
// Save current position. It will help to restore this position once a user
|
||||
// wants to play prev or next stream from the queue
|
||||
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
|
||||
saveStreamProgressState(simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
|
||||
public void saveStreamProgressStateCompleted() {
|
||||
if (currentMetadata != null) {
|
||||
// current stream has ended, so the progress is its duration (+1 to overcome rounding)
|
||||
saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000);
|
||||
}
|
||||
saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue