mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-09-26 19:00:51 +02:00
Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
07111d86d4 | ||
![]() |
ec974a2b3d | ||
![]() |
02906e8132 | ||
![]() |
6f428d0c6b | ||
![]() |
41da2bfb00 | ||
![]() |
746b1f7eb2 | ||
![]() |
03fd286956 | ||
![]() |
fb1b1c5be1 | ||
![]() |
1a8aa8b17e | ||
![]() |
2317864422 | ||
![]() |
0cd1a86aa5 | ||
![]() |
7c39421297 | ||
![]() |
d06cc862c8 | ||
![]() |
c5cf2f4514 | ||
![]() |
3f8e44dc66 | ||
![]() |
d33229a3b8 | ||
![]() |
bb57f9cc9d | ||
![]() |
23a20712da | ||
![]() |
43f46e29ad | ||
![]() |
7617f8cdc7 | ||
![]() |
2e3490bce2 | ||
![]() |
1dd0930b83 | ||
![]() |
265de55a07 | ||
![]() |
d8ed2c8503 | ||
![]() |
73aebc1110 | ||
![]() |
3cb76e4c34 | ||
![]() |
a4767fc48a | ||
![]() |
42d861688e | ||
![]() |
2ee4c6e289 | ||
![]() |
097c2368f4 | ||
![]() |
80e0c6ab0e | ||
![]() |
9067c770a7 | ||
![]() |
f1a071b668 | ||
![]() |
8e888ebdf7 | ||
![]() |
612122997b | ||
![]() |
4b050c0dd8 | ||
![]() |
be4f3d9d62 | ||
![]() |
24ff6a4313 | ||
![]() |
c2968a3ff2 | ||
![]() |
671dd4afd3 | ||
![]() |
600ebdae18 | ||
![]() |
5560cea470 | ||
![]() |
39c500f33c | ||
![]() |
624ad6a47c | ||
![]() |
68ea99d6e6 | ||
![]() |
bc29f40d69 | ||
![]() |
42fb13f17a | ||
![]() |
d5b54c85ed | ||
![]() |
f0307b1b48 | ||
![]() |
75292e099c | ||
![]() |
e0cb2892b8 | ||
![]() |
4c5c2a3d79 | ||
![]() |
e947e86eae | ||
![]() |
5d3955854e | ||
![]() |
3ff4b713e8 | ||
![]() |
68097568d5 | ||
![]() |
cd8d57040c | ||
![]() |
9c82441c19 | ||
![]() |
3d36eb5baf | ||
![]() |
d2d324f2dd | ||
![]() |
ca421c28a1 | ||
![]() |
711345eff7 | ||
![]() |
102975aeb3 | ||
![]() |
c70ce791db | ||
![]() |
444ac5fe95 | ||
![]() |
a69f74f51b | ||
![]() |
e26c038565 | ||
![]() |
9ecd5dff09 | ||
![]() |
ef4a6238c8 | ||
![]() |
b3554a6a49 | ||
![]() |
5fb7b3266b | ||
![]() |
8b6e110635 | ||
![]() |
f5a1f915be | ||
![]() |
ac15339911 | ||
![]() |
fdfeac081a | ||
![]() |
135fc08212 | ||
![]() |
eb3363d4dd |
@@ -20,8 +20,8 @@ android {
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 992
|
||||
versionName "0.25.0"
|
||||
versionCode 993
|
||||
versionName "0.25.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -106,7 +106,7 @@ ext {
|
||||
androidxWorkVersion = '2.7.1'
|
||||
|
||||
icepickVersion = '3.2.0'
|
||||
exoPlayerVersion = '2.18.1'
|
||||
exoPlayerVersion = '2.18.5'
|
||||
googleAutoServiceVersion = '1.0.1'
|
||||
groupieVersion = '2.10.1'
|
||||
markwonVersion = '4.6.2'
|
||||
@@ -191,7 +191,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:7e793c11aec46358ccbfd8bcfcf521105f4f093a'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.22.6'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
|
19
app/sampledata/channels.json
Normal file
19
app/sampledata/channels.json
Normal file
File diff suppressed because one or more lines are too long
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,13 @@ class DatabaseMigrationTest {
|
||||
Migrations.MIGRATION_5_6
|
||||
)
|
||||
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME,
|
||||
Migrations.DB_VER_7,
|
||||
true,
|
||||
Migrations.MIGRATION_6_7
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http|https|market" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -26,7 +27,7 @@ public final class NewPipeDatabase {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
|
||||
MIGRATION_5_6)
|
||||
MIGRATION_5_6, MIGRATION_6_7)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -57,13 +58,9 @@ class AboutActivity : AppCompatActivity() {
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
class AboutFragment : Fragment() {
|
||||
private fun Button.openLink(url: Int) {
|
||||
private fun Button.openLink(@StringRes url: Int) {
|
||||
setOnClickListener {
|
||||
ShareUtils.openUrlInBrowser(
|
||||
context,
|
||||
requireContext().getString(url),
|
||||
false
|
||||
)
|
||||
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -66,7 +66,7 @@ fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||
dialog.dismiss()
|
||||
}
|
||||
setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||
ShareUtils.openUrlInBrowser(context!!, component.link)
|
||||
ShareUtils.openUrlInApp(context!!, component.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_6;
|
||||
import static org.schabi.newpipe.database.Migrations.DB_VER_7;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.RoomDatabase;
|
||||
@@ -38,7 +38,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
|
||||
FeedLastUpdatedEntity.class
|
||||
},
|
||||
version = DB_VER_6
|
||||
version = DB_VER_7
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public static final String DATABASE_NAME = "newpipe.db";
|
||||
|
@@ -24,6 +24,7 @@ public final class Migrations {
|
||||
public static final int DB_VER_4 = 4;
|
||||
public static final int DB_VER_5 = 5;
|
||||
public static final int DB_VER_6 = 6;
|
||||
public static final int DB_VER_7 = 7;
|
||||
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
@@ -197,6 +198,43 @@ public final class Migrations {
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) {
|
||||
@Override
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
// Create a new column thumbnail_stream_id
|
||||
database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` "
|
||||
+ "INTEGER NOT NULL DEFAULT -1");
|
||||
|
||||
// Migrate the thumbnail_url to the thumbnail_stream_id
|
||||
database.execSQL("UPDATE playlists SET thumbnail_stream_id = ("
|
||||
+ " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END"
|
||||
+ " FROM ("
|
||||
+ " SELECT p.uid AS playlist_uid, s.uid AS stream_uid"
|
||||
+ " FROM playlists p"
|
||||
+ " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id"
|
||||
+ " LEFT JOIN streams s ON s.uid = ps.stream_id"
|
||||
+ " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table"
|
||||
+ " WHERE playlist_uid = playlists.uid)");
|
||||
|
||||
// Remove the thumbnail_url field in the playlist table
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`"
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "name TEXT, "
|
||||
+ "is_thumbnail_permanent INTEGER NOT NULL, "
|
||||
+ "thumbnail_stream_id INTEGER NOT NULL)");
|
||||
|
||||
database.execSQL("INSERT INTO playlists_new"
|
||||
+ " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id "
|
||||
+ " FROM playlists");
|
||||
|
||||
|
||||
database.execSQL("DROP TABLE playlists");
|
||||
database.execSQL("ALTER TABLE playlists_new RENAME TO playlists");
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS "
|
||||
+ "`index_playlists_name` ON `playlists` (`name`)");
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() {
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ abstract class FeedDAO {
|
||||
* @return the feed streams filtered according to the conditions provided in the parameters
|
||||
* @see StreamStateEntity.isFinished()
|
||||
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||
* @see StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
@@ -66,6 +67,15 @@ abstract class FeedDAO {
|
||||
OR s.stream_type = 'LIVE_STREAM'
|
||||
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||
)
|
||||
AND (
|
||||
:includePartiallyPlayed
|
||||
OR sh.stream_id IS NULL
|
||||
OR sst.stream_id IS NULL
|
||||
OR (sst.progress_time <= ${StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}
|
||||
AND sst.progress_time <= s.duration * 1000 / 4)
|
||||
OR (sst.progress_time >= s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||
AND sst.progress_time >= s.duration * 1000 * 3 / 4)
|
||||
)
|
||||
AND (
|
||||
:uploadDateBefore IS NULL
|
||||
OR s.upload_date IS NULL
|
||||
@@ -79,6 +89,7 @@ abstract class FeedDAO {
|
||||
abstract fun getStreams(
|
||||
groupId: Long,
|
||||
includePlayed: Boolean,
|
||||
includePartiallyPlayed: Boolean,
|
||||
uploadDateBefore: OffsetDateTime?
|
||||
): Maybe<List<StreamWithState>>
|
||||
|
||||
|
@@ -0,0 +1,24 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
|
||||
/**
|
||||
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
|
||||
* how many times a specific stream is already contained inside a local playlist. Used to be able
|
||||
* to grey out playlists which already contain the current stream in the playlist append dialog.
|
||||
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
|
||||
*/
|
||||
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
|
||||
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
|
||||
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||
public final long timesStreamIsContained;
|
||||
|
||||
public PlaylistDuplicatesEntry(final long uid,
|
||||
final String name,
|
||||
final String thumbnailUrl,
|
||||
final long streamCount,
|
||||
final long timesStreamIsContained) {
|
||||
super(uid, name, thumbnailUrl, streamCount);
|
||||
this.timesStreamIsContained = timesStreamIsContained;
|
||||
}
|
||||
}
|
@@ -6,18 +6,23 @@ import androidx.room.RewriteQueriesToDropUnusedColumns;
|
||||
import androidx.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||
@@ -26,6 +31,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.StreamEntity.STREAM_THUMBNAIL_URL;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
@@ -54,14 +60,15 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||
|
||||
@Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_THUMBNAIL_URL + " ELSE :defaultUrl END"
|
||||
@Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID
|
||||
+ " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END"
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId "
|
||||
+ " LIMIT 1"
|
||||
)
|
||||
Flowable<String> getAutomaticThumbnailUrl(long playlistId, String defaultUrl);
|
||||
Flowable<Long> getAutomaticThumbnailStreamId(long playlistId);
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@@ -84,13 +91,64 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
|
||||
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ","
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||
+ " FROM " + PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
+ " GROUP BY " + PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Transaction
|
||||
@Query("SELECT *, MIN(" + JOIN_INDEX + ")"
|
||||
+ " FROM " + STREAM_TABLE + " INNER JOIN"
|
||||
+ " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_MILLIS
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
+ " GROUP BY " + STREAM_ID
|
||||
+ " ORDER BY MIN(" + JOIN_INDEX + ") ASC")
|
||||
Flowable<List<PlaylistStreamEntry>> getStreamsWithoutDuplicates(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
|
||||
+ PLAYLIST_NAME + ", "
|
||||
|
||||
+ " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = "
|
||||
+ PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'"
|
||||
+ " ELSE (SELECT " + STREAM_THUMBNAIL_URL
|
||||
+ " FROM " + STREAM_TABLE
|
||||
+ " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID
|
||||
+ " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
|
||||
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
|
||||
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
|
||||
|
||||
+ " FROM " + PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
|
||||
+ " LEFT JOIN " + STREAM_TABLE
|
||||
+ " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " AND :streamUrl = :streamUrl"
|
||||
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
|
||||
}
|
||||
|
@@ -8,14 +8,22 @@ import androidx.room.PrimaryKey;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
@Entity(tableName = PLAYLIST_TABLE,
|
||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||
public class PlaylistEntity {
|
||||
|
||||
public static final String DEFAULT_THUMBNAIL = "drawable://"
|
||||
+ R.drawable.placeholder_thumbnail_playlist;
|
||||
public static final long DEFAULT_THUMBNAIL_ID = -1;
|
||||
|
||||
public static final String PLAYLIST_TABLE = "playlists";
|
||||
public static final String PLAYLIST_ID = "uid";
|
||||
public static final String PLAYLIST_NAME = "name";
|
||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent";
|
||||
public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
@@ -24,17 +32,17 @@ public class PlaylistEntity {
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
private String name;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||
private boolean isThumbnailPermanent;
|
||||
|
||||
public PlaylistEntity(final String name, final String thumbnailUrl,
|
||||
final boolean isThumbnailPermanent) {
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||
private long thumbnailStreamId;
|
||||
|
||||
public PlaylistEntity(final String name, final boolean isThumbnailPermanent,
|
||||
final long thumbnailStreamId) {
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.isThumbnailPermanent = isThumbnailPermanent;
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
@@ -53,12 +61,12 @@ public class PlaylistEntity {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
public long getThumbnailStreamId() {
|
||||
return thumbnailStreamId;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
public void setThumbnailStreamId(final long thumbnailStreamId) {
|
||||
this.thumbnailStreamId = thumbnailStreamId;
|
||||
}
|
||||
|
||||
public boolean getIsThumbnailPermanent() {
|
||||
|
@@ -30,7 +30,7 @@ public class StreamStateEntity {
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
|
||||
*/
|
||||
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
|
||||
public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
|
||||
|
||||
/**
|
||||
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||
|
@@ -160,7 +160,7 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
.setMessage(R.string.start_accept_privacy_policy)
|
||||
.setCancelable(false)
|
||||
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
|
||||
ShareUtils.openUrlInBrowser(context,
|
||||
ShareUtils.openUrlInApp(context,
|
||||
context.getString(R.string.privacy_policy_url)))
|
||||
.setPositiveButton(R.string.accept, (dialog, which) -> {
|
||||
if (action.equals("EMAIL")) { // send on email
|
||||
@@ -171,9 +171,9 @@ public class ErrorActivity extends AppCompatActivity {
|
||||
+ getString(R.string.app_name) + " "
|
||||
+ BuildConfig.VERSION_NAME)
|
||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
||||
ShareUtils.openIntentInApp(context, i, true);
|
||||
ShareUtils.openIntentInApp(context, i);
|
||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
||||
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
|
||||
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.decline, (dialog, which) -> {
|
||||
|
@@ -6,7 +6,6 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -144,7 +143,7 @@ class ErrorPanelHelper(
|
||||
*/
|
||||
private fun showAndSetErrorButtonAction(
|
||||
@StringRes resid: Int,
|
||||
@Nullable listener: View.OnClickListener
|
||||
listener: View.OnClickListener
|
||||
) {
|
||||
errorActionButton.isVisible = true
|
||||
errorActionButton.setText(resid)
|
||||
@@ -156,7 +155,7 @@ class ErrorPanelHelper(
|
||||
) {
|
||||
errorOpenInBrowserButton.isVisible = true
|
||||
errorOpenInBrowserButton.setOnClickListener {
|
||||
ShareUtils.openUrlInBrowser(context, errorInfo.request, true)
|
||||
ShareUtils.openUrlInBrowser(context, errorInfo.request)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -7,11 +7,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.DependentPreferenceHelper.getResumePlaybackEnabled;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
|
||||
import static org.schabi.newpipe.util.NavigationHelper.openPlayQueue;
|
||||
import static org.schabi.newpipe.util.NavigationHelper.playWithKore;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -27,6 +26,7 @@ import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -54,9 +54,6 @@ import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
@@ -485,16 +482,8 @@ public final class VideoDetailFragment
|
||||
info.getThumbnailUrl())));
|
||||
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
|
||||
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info -> {
|
||||
try {
|
||||
playWithKore(requireContext(), Uri.parse(info.getUrl()));
|
||||
} catch (final Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.i(TAG, "Failed to start kore", e);
|
||||
}
|
||||
KoreUtils.showInstallKoreDialog(requireContext());
|
||||
}
|
||||
}));
|
||||
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
|
||||
KoreUtils.playWithKore(requireContext(), Uri.parse(info.getUrl()))));
|
||||
if (DEBUG) {
|
||||
binding.detailControlsCrashThePlayer.setOnClickListener(v ->
|
||||
VideoDetailPlayerCrasher.onCrashThePlayer(requireContext(), player));
|
||||
@@ -1457,8 +1446,8 @@ public final class VideoDetailFragment
|
||||
|
||||
animate(binding.detailThumbnailPlayButton, false, 50);
|
||||
animate(binding.detailDurationView, false, 100);
|
||||
animate(binding.detailPositionView, false, 100);
|
||||
animate(binding.positionView, false, 50);
|
||||
binding.detailPositionView.setVisibility(View.GONE);
|
||||
binding.positionView.setVisibility(View.GONE);
|
||||
|
||||
binding.detailVideoTitleView.setText(title);
|
||||
binding.detailVideoTitleView.setMaxLines(1);
|
||||
@@ -1575,7 +1564,7 @@ public final class VideoDetailFragment
|
||||
binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE);
|
||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||
|
||||
updateProgressInfo(info);
|
||||
checkUpdateProgressInfo(info);
|
||||
initThumbnailViews(info);
|
||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||
binding.detailMetaInfoSeparator, disposables);
|
||||
@@ -1674,67 +1663,43 @@ public final class VideoDetailFragment
|
||||
// Stream Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateProgressInfo(@NonNull final StreamInfo info) {
|
||||
private void checkUpdateProgressInfo(@NonNull final StreamInfo info) {
|
||||
if (positionSubscriber != null) {
|
||||
positionSubscriber.dispose();
|
||||
}
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
final boolean playbackResumeEnabled = prefs
|
||||
.getBoolean(activity.getString(R.string.enable_watch_history_key), true)
|
||||
&& prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true);
|
||||
final boolean showPlaybackPosition = prefs.getBoolean(
|
||||
activity.getString(R.string.enable_playback_state_lists_key), true);
|
||||
if (!playbackResumeEnabled) {
|
||||
if (playQueue == null || playQueue.getStreams().isEmpty()
|
||||
|| playQueue.getItem().getRecoveryPosition() == RECOVERY_UNSET
|
||||
|| !showPlaybackPosition) {
|
||||
binding.positionView.setVisibility(View.INVISIBLE);
|
||||
binding.detailPositionView.setVisibility(View.GONE);
|
||||
// TODO: Remove this check when separation of concerns is done.
|
||||
// (live streams weren't getting updated because they are mixed)
|
||||
if (!StreamTypeUtil.isLiveStream(info.getStreamType())) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Show saved position from backStack if user allows it
|
||||
showPlaybackProgress(playQueue.getItem().getRecoveryPosition(),
|
||||
playQueue.getItem().getDuration() * 1000);
|
||||
animate(binding.positionView, true, 500);
|
||||
animate(binding.detailPositionView, true, 500);
|
||||
}
|
||||
if (!getResumePlaybackEnabled(activity)) {
|
||||
binding.positionView.setVisibility(View.GONE);
|
||||
binding.detailPositionView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||
|
||||
// TODO: Separate concerns when updating database data.
|
||||
// (move the updating part to when the loading happens)
|
||||
positionSubscriber = recordManager.loadStreamState(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.onErrorComplete()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(state -> {
|
||||
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
|
||||
animate(binding.positionView, true, 500);
|
||||
animate(binding.detailPositionView, true, 500);
|
||||
updatePlaybackProgress(
|
||||
state.getProgressMillis(), info.getDuration() * 1000);
|
||||
}, e -> {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// impossible since the onErrorComplete()
|
||||
}, () -> {
|
||||
binding.positionView.setVisibility(View.GONE);
|
||||
binding.detailPositionView.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
|
||||
private void showPlaybackProgress(final long progress, final long duration) {
|
||||
private void updatePlaybackProgress(final long progress, final long duration) {
|
||||
if (!getResumePlaybackEnabled(activity)) {
|
||||
return;
|
||||
}
|
||||
final int progressSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(progress);
|
||||
final int durationSeconds = (int) TimeUnit.MILLISECONDS.toSeconds(duration);
|
||||
// If the old and the new progress values have a big difference then use
|
||||
// animation. Otherwise don't because it affects CPU
|
||||
final boolean shouldAnimate = Math.abs(binding.positionView.getProgress()
|
||||
- progressSeconds) > 2;
|
||||
// If the old and the new progress values have a big difference then use animation.
|
||||
// Otherwise don't because it affects CPU
|
||||
final int progressDifference = Math.abs(binding.positionView.getProgress()
|
||||
- progressSeconds);
|
||||
binding.positionView.setMax(durationSeconds);
|
||||
if (shouldAnimate) {
|
||||
if (progressDifference > 2) {
|
||||
binding.positionView.setProgressAnimated(progressSeconds);
|
||||
} else {
|
||||
binding.positionView.setProgress(progressSeconds);
|
||||
@@ -1829,7 +1794,7 @@ public final class VideoDetailFragment
|
||||
}
|
||||
|
||||
if (player.getPlayQueue().getItem().getUrl().equals(url)) {
|
||||
showPlaybackProgress(currentProgress, duration);
|
||||
updatePlaybackProgress(currentProgress, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1961,17 +1926,15 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
final var window = activity.getWindow();
|
||||
final var windowInsetsController = WindowCompat.getInsetsController(window,
|
||||
window.getDecorView());
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true);
|
||||
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat
|
||||
.BEHAVIOR_SHOW_BARS_BY_TOUCH);
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
window.setStatusBarColor(ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||
android.R.attr.colorPrimary));
|
||||
// Prevent jumping of the player on devices with cutout
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
|
||||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(0);
|
||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
|
||||
requireContext(), android.R.attr.colorPrimary));
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
@@ -1983,19 +1946,30 @@ public final class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
|
||||
final var window = activity.getWindow();
|
||||
final var windowInsetsController = WindowCompat.getInsetsController(window,
|
||||
window.getDecorView());
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false);
|
||||
windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat
|
||||
.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
if (DeviceUtils.isInMultiWindow(activity) || isFullscreen()) {
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
// Prevent jumping of the player on devices with cutout
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
// In multiWindow mode status bar is not transparent for devices with cutout
|
||||
// if I include this flag. So without it is better in this case
|
||||
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
|
||||
if (!isInMultiWindow) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
}
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
|
||||
if (isInMultiWindow || isFullscreen()) {
|
||||
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||
}
|
||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
// Listener implementation
|
||||
|
@@ -204,8 +204,7 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
|
||||
break;
|
||||
case R.id.menu_item_rss:
|
||||
if (currentInfo != null) {
|
||||
ShareUtils.openUrlInBrowser(
|
||||
requireContext(), currentInfo.getFeedUrl(), false);
|
||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||
}
|
||||
break;
|
||||
case R.id.menu_item_openInBrowser:
|
||||
|
@@ -17,6 +17,7 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||
@@ -73,6 +74,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
|
||||
private static final int CHANNEL_HOLDER_TYPE = 0x201;
|
||||
private static final int GRID_CHANNEL_HOLDER_TYPE = 0x202;
|
||||
private static final int CARD_CHANNEL_HOLDER_TYPE = 0x203;
|
||||
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||
private static final int GRID_PLAYLIST_HOLDER_TYPE = 0x302;
|
||||
@@ -249,7 +251,9 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return STREAM_HOLDER_TYPE;
|
||||
}
|
||||
case CHANNEL:
|
||||
if (itemMode == ItemViewMode.GRID) {
|
||||
if (itemMode == ItemViewMode.CARD) {
|
||||
return CARD_CHANNEL_HOLDER_TYPE;
|
||||
} else if (itemMode == ItemViewMode.GRID) {
|
||||
return GRID_CHANNEL_HOLDER_TYPE;
|
||||
} else if (useMiniVariant) {
|
||||
return MINI_CHANNEL_HOLDER_TYPE;
|
||||
@@ -304,6 +308,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||
case CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||
case CARD_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
|
||||
case GRID_CHANNEL_HOLDER_TYPE:
|
||||
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
|
||||
case MINI_PLAYLIST_HOLDER_TYPE:
|
||||
|
@@ -99,14 +99,8 @@ public enum StreamDialogDefaultEntry {
|
||||
)
|
||||
),
|
||||
|
||||
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) -> {
|
||||
final Uri videoUrl = Uri.parse(item.getUrl());
|
||||
try {
|
||||
NavigationHelper.playWithKore(fragment.requireContext(), videoUrl);
|
||||
} catch (final Exception e) {
|
||||
KoreUtils.showInstallKoreDialog(fragment.requireActivity());
|
||||
}
|
||||
}),
|
||||
PLAY_WITH_KODI(R.string.play_with_kodi_title, (fragment, item) ->
|
||||
KoreUtils.playWithKore(fragment.requireContext(), Uri.parse(item.getUrl()))),
|
||||
|
||||
SHARE(R.string.share, (fragment, item) ->
|
||||
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
|
||||
|
@@ -0,0 +1,22 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class ChannelCardInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
public ChannelCardInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||
final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_channel_card_item, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDescriptionMaxLineCount(@Nullable final String content) {
|
||||
// Based on `list_channel_card_item` left side content (thumbnail 100dp
|
||||
// + additional details), Right side description can grow up to 8 lines.
|
||||
return 8;
|
||||
}
|
||||
}
|
@@ -46,6 +46,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
itemTitleView.setText(item.getName());
|
||||
itemTitleView.setSelected(true);
|
||||
|
||||
final String detailLine = getDetailLine(item);
|
||||
if (detailLine == null) {
|
||||
@@ -77,11 +78,24 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
} else {
|
||||
itemChannelDescriptionView.setVisibility(View.VISIBLE);
|
||||
itemChannelDescriptionView.setText(item.getDescription());
|
||||
itemChannelDescriptionView.setMaxLines(detailLine == null ? 3 : 2);
|
||||
// setMaxLines utilize the line space for description if the additional details
|
||||
// (sub / video count) are not present.
|
||||
// Case1: 2 lines of description + 1 line additional details
|
||||
// Case2: 3 lines of description (additionalDetails is GONE)
|
||||
itemChannelDescriptionView.setMaxLines(getDescriptionMaxLineCount(detailLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns max number of allowed lines for the description field.
|
||||
* @param content additional detail content (video / sub count)
|
||||
* @return max line count
|
||||
*/
|
||||
protected int getDescriptionMaxLineCount(@Nullable final String content) {
|
||||
return content == null ? 3 : 2;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDetailLine(final ChannelInfoItem item) {
|
||||
if (item.getStreamCount() >= 0 && item.getSubscriberCount() >= 0) {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.text.Layout;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
@@ -59,9 +60,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
private final TextView itemPublishedTime;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Description commentText;
|
||||
private StreamingService streamService;
|
||||
private String streamUrl;
|
||||
@Nullable private Description commentText;
|
||||
@Nullable private StreamingService streamService;
|
||||
@Nullable private String streamUrl;
|
||||
|
||||
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
|
||||
final ViewGroup parent) {
|
||||
@@ -153,15 +154,17 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
if (DeviceUtils.isTv(itemBuilder.getContext())) {
|
||||
openCommentAuthor(item);
|
||||
} else {
|
||||
ShareUtils.copyToClipboard(itemBuilder.getContext(),
|
||||
itemContentView.getText().toString());
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (text != null) {
|
||||
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void openCommentAuthor(final CommentsInfoItem item) {
|
||||
if (TextUtils.isEmpty(item.getUploaderUrl())) {
|
||||
if (isEmpty(item.getUploaderUrl())) {
|
||||
return;
|
||||
}
|
||||
final AppCompatActivity activity = (AppCompatActivity) itemBuilder.getContext();
|
||||
@@ -207,11 +210,12 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
linkifyCommentContentView(v -> {
|
||||
boolean hasEllipsis = false;
|
||||
|
||||
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
final CharSequence charSeqText = itemContentView.getText();
|
||||
if (charSeqText != null && itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
// Note that converting to String removes spans (i.e. links), but that's something
|
||||
// we actually want since when the text is ellipsized we want all clicks on the
|
||||
// comment to expand the comment, not to open links.
|
||||
final String text = itemContentView.getText().toString();
|
||||
final String text = charSeqText.toString();
|
||||
|
||||
final Layout layout = itemContentView.getLayout();
|
||||
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
|
||||
@@ -252,7 +256,7 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
private void toggleEllipsize() {
|
||||
final CharSequence text = itemContentView.getText();
|
||||
if (text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
||||
if (!isEmpty(text) && text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
|
||||
expand();
|
||||
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
|
||||
ellipsize();
|
||||
|
@@ -14,6 +14,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.ktx.ViewUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.PicassoHelper;
|
||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||
@@ -60,8 +61,12 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
|
||||
final StreamStateEntity state2 = historyRecordManager.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
StreamStateEntity state2 = null;
|
||||
if (DependentPreferenceHelper
|
||||
.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state2 = historyRecordManager.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
}
|
||||
if (state2 != null) {
|
||||
itemProgressView.setVisibility(View.VISIBLE);
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
@@ -111,9 +116,12 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
final HistoryRecordManager historyRecordManager) {
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
final StreamStateEntity state = historyRecordManager
|
||||
.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
StreamStateEntity state = null;
|
||||
if (DependentPreferenceHelper.getPositionsInListsEnabled(itemProgressView.getContext())) {
|
||||
state = historyRecordManager
|
||||
.loadStreamState(infoItem)
|
||||
.blockingGet()[0];
|
||||
}
|
||||
if (state != null && item.getDuration() > 0
|
||||
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
|
||||
itemProgressView.setMax((int) item.getDuration());
|
||||
|
@@ -280,10 +280,10 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
|
||||
showDeleteDialog(selectedItem.name,
|
||||
localPlaylistManager.deletePlaylist(selectedItem.uid));
|
||||
} else if (isThumbnailPermanent && items.get(index).equals(unsetThumbnail)) {
|
||||
final String thumbnailUrl = localPlaylistManager
|
||||
.getAutomaticPlaylistThumbnail(selectedItem.uid);
|
||||
final long thumbnailStreamId = localPlaylistManager
|
||||
.getAutomaticPlaylistThumbnailStreamId(selectedItem.uid);
|
||||
localPlaylistManager
|
||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailUrl, false)
|
||||
.changePlaylistThumbnail(selectedItem.uid, thumbnailStreamId, false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe();
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -13,7 +14,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
@@ -28,6 +30,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private RecyclerView playlistRecyclerView;
|
||||
private LocalItemListAdapter playlistAdapter;
|
||||
private TextView playlistDuplicateIndicator;
|
||||
|
||||
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
|
||||
|
||||
@@ -63,8 +66,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||
playlistAdapter.setSelectedListener(selectedItem -> {
|
||||
final List<StreamEntity> entities = getStreamEntities();
|
||||
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
|
||||
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
|
||||
if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
|
||||
onPlaylistSelected(playlistManager,
|
||||
(PlaylistDuplicatesEntry) selectedItem, entities);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,10 +76,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
playlistRecyclerView.setAdapter(playlistAdapter);
|
||||
|
||||
playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
|
||||
|
||||
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
|
||||
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
|
||||
|
||||
playlistDisposables.add(playlistManager.getPlaylists()
|
||||
playlistDisposables.add(playlistManager
|
||||
.getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onPlaylistsReceived));
|
||||
}
|
||||
@@ -117,31 +124,50 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
||||
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
|
||||
if (playlistAdapter != null && playlistRecyclerView != null) {
|
||||
private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
|
||||
if (playlistAdapter != null
|
||||
&& playlistRecyclerView != null
|
||||
&& playlistDuplicateIndicator != null) {
|
||||
playlistAdapter.clearStreamItemList();
|
||||
playlistAdapter.addItems(playlists);
|
||||
playlistRecyclerView.setVisibility(View.VISIBLE);
|
||||
playlistDuplicateIndicator.setVisibility(
|
||||
anyPlaylistContainsDuplicates(playlists) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistMetadataEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
final Toast successToast = Toast.makeText(getContext(),
|
||||
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
|
||||
return playlists.stream()
|
||||
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
|
||||
}
|
||||
|
||||
if (playlist.thumbnailUrl
|
||||
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl(), false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
|
||||
@NonNull final PlaylistDuplicatesEntry playlist,
|
||||
@NonNull final List<StreamEntity> streams) {
|
||||
|
||||
final String toastText;
|
||||
if (playlist.timesStreamIsContained > 0) {
|
||||
toastText = getString(R.string.playlist_add_stream_success_duplicate,
|
||||
playlist.timesStreamIsContained);
|
||||
} else {
|
||||
toastText = getString(R.string.playlist_add_stream_success);
|
||||
}
|
||||
|
||||
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
|
||||
|
||||
playlistDisposables.add(manager.appendToPlaylist(playlist.uid, streams)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> successToast.show()));
|
||||
.subscribe(ignored -> {
|
||||
successToast.show();
|
||||
|
||||
if (playlist.thumbnailUrl.equals(PlaylistEntity.DEFAULT_THUMBNAIL)) {
|
||||
playlistDisposables.add(manager
|
||||
.changePlaylistThumbnail(playlist.uid, streams.get(0).getUid(),
|
||||
false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignore -> successToast.show()));
|
||||
}
|
||||
}));
|
||||
|
||||
requireDialog().dismiss();
|
||||
}
|
||||
|
@@ -43,11 +43,13 @@ class FeedDatabaseManager(context: Context) {
|
||||
fun getStreams(
|
||||
groupId: Long,
|
||||
includePlayedStreams: Boolean,
|
||||
includePartiallyPlayedStreams: Boolean,
|
||||
includeFutureStreams: Boolean
|
||||
): Maybe<List<StreamWithState>> {
|
||||
return feedTable.getStreams(
|
||||
groupId,
|
||||
includePlayedStreams,
|
||||
includePartiallyPlayedStreams,
|
||||
if (includeFutureStreams) null else OffsetDateTime.now()
|
||||
)
|
||||
}
|
||||
|
@@ -37,11 +37,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
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
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
@@ -100,8 +98,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
||||
|
||||
private lateinit var groupAdapter: GroupieAdapter
|
||||
@State @JvmField var showPlayedItems: Boolean = true
|
||||
@State @JvmField var showFutureItems: Boolean = true
|
||||
|
||||
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
||||
private var updateListViewModeOnResume = false
|
||||
@@ -140,8 +136,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
|
||||
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
||||
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
|
||||
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
||||
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
|
||||
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
||||
|
||||
groupAdapter = GroupieAdapter().apply {
|
||||
@@ -216,8 +210,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
activity.supportActionBar?.subtitle = groupName
|
||||
|
||||
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
||||
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
||||
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -243,20 +235,43 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
.show()
|
||||
return true
|
||||
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
|
||||
showPlayedItems = !item.isChecked
|
||||
updateTogglePlayedItemsButton(item)
|
||||
viewModel.togglePlayedItems(showPlayedItems)
|
||||
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
||||
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
|
||||
showFutureItems = !item.isChecked
|
||||
updateToggleFutureItemsButton(item)
|
||||
viewModel.toggleFutureItems(showFutureItems)
|
||||
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
|
||||
showStreamVisibilityDialog()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun showStreamVisibilityDialog() {
|
||||
val dialogItems = arrayOf(
|
||||
getString(R.string.feed_show_watched),
|
||||
getString(R.string.feed_show_partially_watched),
|
||||
getString(R.string.feed_show_upcoming)
|
||||
)
|
||||
|
||||
val checkedDialogItems = booleanArrayOf(
|
||||
viewModel.getShowPlayedItemsFromPreferences(),
|
||||
viewModel.getShowPartiallyPlayedItemsFromPreferences(),
|
||||
viewModel.getShowFutureItemsFromPreferences()
|
||||
)
|
||||
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
builder.setTitle(R.string.feed_hide_streams_title)
|
||||
builder.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||
checkedDialogItems[which] = isChecked
|
||||
}
|
||||
|
||||
builder.setPositiveButton(R.string.ok) { _, _ ->
|
||||
viewModel.setSaveShowPlayedItems(checkedDialogItems[0])
|
||||
|
||||
viewModel.setSaveShowPartiallyPlayedItems(checkedDialogItems[1])
|
||||
|
||||
viewModel.setSaveShowFutureItems(checkedDialogItems[2])
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel, null)
|
||||
|
||||
builder.create().show()
|
||||
}
|
||||
|
||||
override fun onDestroyOptionsMenu() {
|
||||
super.onDestroyOptionsMenu()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
@@ -283,40 +298,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
||||
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
|
||||
)
|
||||
MenuItemCompat.setTooltipText(
|
||||
menuItem,
|
||||
getString(
|
||||
if (showPlayedItems)
|
||||
R.string.feed_toggle_hide_played_items
|
||||
else
|
||||
R.string.feed_toggle_show_played_items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
|
||||
menuItem.isChecked = showFutureItems
|
||||
menuItem.icon = AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
|
||||
)
|
||||
MenuItemCompat.setTooltipText(
|
||||
menuItem,
|
||||
getString(
|
||||
if (showFutureItems)
|
||||
R.string.feed_toggle_hide_future_items
|
||||
else
|
||||
R.string.feed_toggle_show_future_items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////////
|
||||
// Handling
|
||||
// //////////////////////////////////////////////////////////////////////////
|
||||
|
@@ -11,7 +11,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.functions.Function5
|
||||
import io.reactivex.rxjava3.functions.Function6
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.schabi.newpipe.App
|
||||
@@ -31,18 +31,24 @@ import java.util.concurrent.TimeUnit
|
||||
class FeedViewModel(
|
||||
private val application: Application,
|
||||
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
|
||||
initialShowPlayedItems: Boolean = true,
|
||||
initialShowFutureItems: Boolean = true
|
||||
initialShowPlayedItems: Boolean,
|
||||
initialShowPartiallyPlayedItems: Boolean,
|
||||
initialShowFutureItems: Boolean
|
||||
) : ViewModel() {
|
||||
private val feedDatabaseManager = FeedDatabaseManager(application)
|
||||
|
||||
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
|
||||
private val showPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val showPlayedItemsFlowable = showPlayedItems
|
||||
.startWithItem(initialShowPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>()
|
||||
private val toggleShowFutureItemsFlowable = toggleShowFutureItems
|
||||
private val showPartiallyPlayedItems = BehaviorProcessor.create<Boolean>()
|
||||
private val showPartiallyPlayedItemsFlowable = showPartiallyPlayedItems
|
||||
.startWithItem(initialShowPartiallyPlayedItems)
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val showFutureItems = BehaviorProcessor.create<Boolean>()
|
||||
private val showFutureItemsFlowable = showFutureItems
|
||||
.startWithItem(initialShowFutureItems)
|
||||
.distinctUntilChanged()
|
||||
|
||||
@@ -52,23 +58,24 @@ class FeedViewModel(
|
||||
private var combineDisposable = Flowable
|
||||
.combineLatest(
|
||||
FeedEventManager.events(),
|
||||
toggleShowPlayedItemsFlowable,
|
||||
toggleShowFutureItemsFlowable,
|
||||
showPlayedItemsFlowable,
|
||||
showPartiallyPlayedItemsFlowable,
|
||||
showFutureItemsFlowable,
|
||||
feedDatabaseManager.notLoadedCount(groupId),
|
||||
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
|
||||
|
||||
Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
|
||||
t4: Long, t5: List<OffsetDateTime> ->
|
||||
return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
|
||||
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean,
|
||||
t5: Long, t6: List<OffsetDateTime> ->
|
||||
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
|
||||
}
|
||||
)
|
||||
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
|
||||
.map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
|
||||
val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
|
||||
feedDatabaseManager
|
||||
.getStreams(groupId, showPlayedItems, showFutureItems)
|
||||
.getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems)
|
||||
.blockingGet(arrayListOf())
|
||||
else
|
||||
arrayListOf()
|
||||
@@ -100,8 +107,9 @@ class FeedViewModel(
|
||||
val t1: FeedEventManager.Event,
|
||||
val t2: Boolean,
|
||||
val t3: Boolean,
|
||||
val t4: Long,
|
||||
val t5: OffsetDateTime?
|
||||
val t4: Boolean,
|
||||
val t5: Long,
|
||||
val t6: OffsetDateTime?
|
||||
)
|
||||
|
||||
private data class CombineResultDataHolder(
|
||||
@@ -111,37 +119,49 @@ class FeedViewModel(
|
||||
val t4: OffsetDateTime?
|
||||
)
|
||||
|
||||
fun togglePlayedItems(showPlayedItems: Boolean) {
|
||||
toggleShowPlayedItems.onNext(showPlayedItems)
|
||||
}
|
||||
|
||||
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
|
||||
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
||||
this.showPlayedItems.onNext(showPlayedItems)
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||
this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
|
||||
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application)
|
||||
|
||||
fun toggleFutureItems(showFutureItems: Boolean) {
|
||||
toggleShowFutureItems.onNext(showFutureItems)
|
||||
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
||||
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
||||
this.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) =
|
||||
fun getShowPartiallyPlayedItemsFromPreferences() = getShowPartiallyPlayedItemsFromPreferences(application)
|
||||
|
||||
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
||||
this.showFutureItems.onNext(showFutureItems)
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
||||
this.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
|
||||
|
||||
companion object {
|
||||
private fun getShowPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
|
||||
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
|
||||
|
||||
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
|
||||
|
||||
private fun getShowFutureItemsFromPreferences(context: Context) =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
|
||||
|
||||
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
|
||||
initializer {
|
||||
FeedViewModel(
|
||||
@@ -149,6 +169,7 @@ class FeedViewModel(
|
||||
groupId,
|
||||
// Read initial value from preferences
|
||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||
getShowPartiallyPlayedItemsFromPreferences(context.applicationContext),
|
||||
getShowFutureItemsFromPreferences(context.applicationContext)
|
||||
)
|
||||
}
|
||||
|
@@ -87,7 +87,7 @@ public class HistoryRecordManager {
|
||||
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
|
||||
* hidden. Adds a history entry and updates the stream progress to 100%.
|
||||
*
|
||||
* @see FeedViewModel#togglePlayedItems
|
||||
* @see FeedViewModel#setSaveShowPlayedItems
|
||||
* @param info the item to mark as watched
|
||||
* @return a Maybe containing the ID of the item if successful
|
||||
*/
|
||||
|
@@ -4,6 +4,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
@@ -13,6 +14,9 @@ import org.schabi.newpipe.util.Localization;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
|
||||
private static final float GRAYED_OUT_ALPHA = 0.6f;
|
||||
|
||||
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
|
||||
super(infoItemBuilder, parent);
|
||||
}
|
||||
@@ -38,6 +42,13 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
|
||||
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
|
||||
|
||||
if (item instanceof PlaylistDuplicatesEntry
|
||||
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
|
||||
itemView.setAlpha(GRAYED_OUT_ALPHA);
|
||||
} else {
|
||||
itemView.setAlpha(1.0f);
|
||||
}
|
||||
|
||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user