Merge branch 'play' into dev
|
@ -55,7 +55,7 @@ dependencies {
|
||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7fd21ec08581d'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:4fb49d54b5'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||||
|
|
|
@ -17,7 +17,6 @@ public class DebugApp extends App {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
initStetho();
|
initStetho();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||||
|
|
||||||
|
@ -80,8 +78,7 @@ public class App extends Application {
|
||||||
initNotificationChannel();
|
initNotificationChannel();
|
||||||
|
|
||||||
// Initialize image loader
|
// Initialize image loader
|
||||||
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
|
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50));
|
||||||
ImageLoader.getInstance().init(config);
|
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
configureRxJavaErrorHandler();
|
||||||
}
|
}
|
||||||
|
@ -119,6 +116,14 @@ public class App extends Application {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||||
|
final int diskCacheSizeMb) {
|
||||||
|
return new ImageLoaderConfiguration.Builder(this)
|
||||||
|
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||||
|
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private void initACRA() {
|
private void initACRA() {
|
||||||
try {
|
try {
|
||||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||||
|
|
|
@ -26,8 +26,6 @@ import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.view.GravityCompat;
|
import android.support.v4.view.GravityCompat;
|
||||||
|
@ -42,40 +40,21 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
|
||||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
|
||||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.MainFragment;
|
import org.schabi.newpipe.fragments.MainFragment;
|
||||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.history.HistoryListener;
|
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.Date;
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import io.reactivex.subjects.PublishSubject;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity implements HistoryListener {
|
|
||||||
private static final String TAG = "MainActivity";
|
private static final String TAG = "MainActivity";
|
||||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||||
|
|
||||||
private SharedPreferences sharedPreferences;
|
|
||||||
private ActionBarDrawerToggle toggle = null;
|
private ActionBarDrawerToggle toggle = null;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -86,7 +65,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -98,7 +76,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
|
|
||||||
setSupportActionBar(findViewById(R.id.toolbar));
|
setSupportActionBar(findViewById(R.id.toolbar));
|
||||||
setupDrawer();
|
setupDrawer();
|
||||||
initHistory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupDrawer() {
|
private void setupDrawer() {
|
||||||
|
@ -149,8 +126,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
if (!isChangingConfigurations()) {
|
if (!isChangingConfigurations()) {
|
||||||
StateSaver.clearStateFiles();
|
StateSaver.clearStateFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeHistory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -357,75 +332,4 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// History
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
private WatchHistoryDAO watchHistoryDAO;
|
|
||||||
private SearchHistoryDAO searchHistoryDAO;
|
|
||||||
private PublishSubject<HistoryEntry> historyEntrySubject;
|
|
||||||
private Disposable disposable;
|
|
||||||
|
|
||||||
private void initHistory() {
|
|
||||||
final AppDatabase database = NewPipeDatabase.getInstance();
|
|
||||||
watchHistoryDAO = database.watchHistoryDAO();
|
|
||||||
searchHistoryDAO = database.searchHistoryDAO();
|
|
||||||
historyEntrySubject = PublishSubject.create();
|
|
||||||
disposable = historyEntrySubject
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe(getHistoryEntryConsumer());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disposeHistory() {
|
|
||||||
if (disposable != null) disposable.dispose();
|
|
||||||
watchHistoryDAO = null;
|
|
||||||
searchHistoryDAO = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Consumer<HistoryEntry> getHistoryEntryConsumer() {
|
|
||||||
return new Consumer<HistoryEntry>() {
|
|
||||||
@Override
|
|
||||||
public void accept(HistoryEntry historyEntry) throws Exception {
|
|
||||||
//noinspection unchecked
|
|
||||||
HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>)
|
|
||||||
(historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO);
|
|
||||||
|
|
||||||
HistoryEntry latestEntry = historyDAO.getLatestEntry();
|
|
||||||
if (historyEntry.hasEqualValues(latestEntry)) {
|
|
||||||
latestEntry.setCreationDate(historyEntry.getCreationDate());
|
|
||||||
historyDAO.update(latestEntry);
|
|
||||||
} else {
|
|
||||||
historyDAO.insert(historyEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addWatchHistoryEntry(StreamInfo streamInfo) {
|
|
||||||
if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) {
|
|
||||||
WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo);
|
|
||||||
historyEntrySubject.onNext(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) {
|
|
||||||
addWatchHistoryEntry(streamInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) {
|
|
||||||
addWatchHistoryEntry(streamInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSearch(int serviceId, String query) {
|
|
||||||
// Add search history entry
|
|
||||||
if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) {
|
|
||||||
SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query);
|
|
||||||
historyEntrySubject.onNext(searchHistoryEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.support.annotation.NonNull;
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
|
||||||
|
import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
|
||||||
|
|
||||||
public final class NewPipeDatabase {
|
public final class NewPipeDatabase {
|
||||||
|
|
||||||
|
@ -17,15 +18,24 @@ public final class NewPipeDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Context context) {
|
public static void init(Context context) {
|
||||||
databaseInstance = Room.databaseBuilder(context.getApplicationContext(),
|
databaseInstance = Room
|
||||||
AppDatabase.class, DATABASE_NAME
|
.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
|
||||||
).build();
|
.addMigrations(MIGRATION_11_12)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Deprecated
|
||||||
public static AppDatabase getInstance() {
|
public static AppDatabase getInstance() {
|
||||||
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
||||||
|
|
||||||
return databaseInstance;
|
return databaseInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static AppDatabase getInstance(Context context) {
|
||||||
|
if (databaseInstance == null) init(context);
|
||||||
|
return databaseInstance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,52 @@ import android.arch.persistence.room.Database;
|
||||||
import android.arch.persistence.room.RoomDatabase;
|
import android.arch.persistence.room.RoomDatabase;
|
||||||
import android.arch.persistence.room.TypeConverters;
|
import android.arch.persistence.room.TypeConverters;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.Converters;
|
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.Migrations.DB_VER_12_0;
|
||||||
|
|
||||||
@TypeConverters({Converters.class})
|
@TypeConverters({Converters.class})
|
||||||
@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false)
|
@Database(
|
||||||
|
entities = {
|
||||||
|
SubscriptionEntity.class, SearchHistoryEntry.class,
|
||||||
|
StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class,
|
||||||
|
PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class
|
||||||
|
},
|
||||||
|
version = DB_VER_12_0,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public static final String DATABASE_NAME = "newpipe.db";
|
public static final String DATABASE_NAME = "newpipe.db";
|
||||||
|
|
||||||
public abstract SubscriptionDAO subscriptionDAO();
|
public abstract SubscriptionDAO subscriptionDAO();
|
||||||
|
|
||||||
public abstract WatchHistoryDAO watchHistoryDAO();
|
|
||||||
|
|
||||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||||
|
|
||||||
|
public abstract StreamDAO streamDAO();
|
||||||
|
|
||||||
|
public abstract StreamHistoryDAO streamHistoryDAO();
|
||||||
|
|
||||||
|
public abstract StreamStateDAO streamStateDAO();
|
||||||
|
|
||||||
|
public abstract PlaylistDAO playlistDAO();
|
||||||
|
|
||||||
|
public abstract PlaylistStreamDAO playlistStreamDAO();
|
||||||
|
|
||||||
|
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,6 @@ public interface BasicDAO<Entity> {
|
||||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||||
List<Long> insertAll(final Collection<Entity> entities);
|
List<Long> insertAll(final Collection<Entity> entities);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
long upsert(final Entity entity);
|
|
||||||
|
|
||||||
/* Searches */
|
/* Searches */
|
||||||
Flowable<List<Entity>> getAll();
|
Flowable<List<Entity>> getAll();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package org.schabi.newpipe.database.history;
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
import android.arch.persistence.room.TypeConverter;
|
import android.arch.persistence.room.TypeConverter;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class Converters {
|
public class Converters {
|
||||||
|
@ -25,4 +27,14 @@ public class Converters {
|
||||||
public static Long dateToTimestamp(Date date) {
|
public static Long dateToTimestamp(Date date) {
|
||||||
return date == null ? null : date.getTime();
|
return date == null ? null : date.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static StreamType streamTypeOf(String value) {
|
||||||
|
return StreamType.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String stringOf(StreamType streamType) {
|
||||||
|
return streamType.name();
|
||||||
|
}
|
||||||
}
|
}
|
13
app/src/main/java/org/schabi/newpipe/database/LocalItem.java
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
|
public interface LocalItem {
|
||||||
|
enum LocalItemType {
|
||||||
|
PLAYLIST_LOCAL_ITEM,
|
||||||
|
PLAYLIST_REMOTE_ITEM,
|
||||||
|
|
||||||
|
PLAYLIST_STREAM_ITEM,
|
||||||
|
STATISTIC_STREAM_ITEM,
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalItemType getLocalItemType();
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.schabi.newpipe.database;
|
||||||
|
|
||||||
|
import android.arch.persistence.db.SupportSQLiteDatabase;
|
||||||
|
import android.arch.persistence.room.migration.Migration;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class Migrations {
|
||||||
|
|
||||||
|
public static final int DB_VER_11_0 = 1;
|
||||||
|
public static final int DB_VER_12_0 = 2;
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_11_12 = new Migration(DB_VER_11_0, DB_VER_12_0) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
/*
|
||||||
|
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||||
|
* schema and names changing at a later date, thus invalidating the older migration
|
||||||
|
* scripts if they are not hardcoded.
|
||||||
|
* */
|
||||||
|
|
||||||
|
// Not much we can do about this, since room doesn't create tables before migration.
|
||||||
|
// It's either this or blasting the entire database anew.
|
||||||
|
database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||||
|
database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)");
|
||||||
|
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||||
|
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||||
|
database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)");
|
||||||
|
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)");
|
||||||
|
|
||||||
|
// Populate streams table with existing entries in watch history
|
||||||
|
// Latest data first, thus ignoring older entries with the same indices
|
||||||
|
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " +
|
||||||
|
"stream_type, duration, uploader, thumbnail_url) " +
|
||||||
|
|
||||||
|
"SELECT service_id, url, title, 'VIDEO_STREAM', duration, " +
|
||||||
|
"uploader, thumbnail_url " +
|
||||||
|
|
||||||
|
"FROM watch_history " +
|
||||||
|
"ORDER BY creation_date DESC");
|
||||||
|
|
||||||
|
// Once the streams have PKs, join them with the normalized history table
|
||||||
|
// and populate it with the remaining data from watch history
|
||||||
|
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" +
|
||||||
|
"SELECT uid, creation_date, 1 " +
|
||||||
|
"FROM watch_history INNER JOIN streams " +
|
||||||
|
"ON watch_history.service_id == streams.service_id " +
|
||||||
|
"AND watch_history.url == streams.url " +
|
||||||
|
"ORDER BY creation_date DESC");
|
||||||
|
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ package org.schabi.newpipe.database.history.dao;
|
||||||
|
|
||||||
import android.arch.persistence.room.Dao;
|
import android.arch.persistence.room.Dao;
|
||||||
import android.arch.persistence.room.Query;
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -20,8 +22,9 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||||
|
|
||||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
@Query("SELECT * FROM " + TABLE_NAME +
|
||||||
@Override
|
" WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||||
|
@Nullable
|
||||||
SearchHistoryEntry getLatestEntry();
|
SearchHistoryEntry getLatestEntry();
|
||||||
|
|
||||||
@Query("DELETE FROM " + TABLE_NAME)
|
@Query("DELETE FROM " + TABLE_NAME)
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package org.schabi.newpipe.database.history.dao;
|
||||||
|
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
||||||
|
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||||
|
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.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||||
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE +
|
||||||
|
" WHERE " + STREAM_ACCESS_DATE + " = " +
|
||||||
|
"(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public abstract StreamHistoryEntity getLatestEntry();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE)
|
||||||
|
public abstract Flowable<List<StreamHistoryEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||||
|
" INNER JOIN " + STREAM_HISTORY_TABLE +
|
||||||
|
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||||
|
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||||
|
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
|
public abstract int deleteStreamHistory(final long streamId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||||
|
|
||||||
|
// Select the latest entry and watch count for each stream id on history table
|
||||||
|
" INNER JOIN " +
|
||||||
|
"(SELECT " + JOIN_STREAM_ID + ", " +
|
||||||
|
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
|
||||||
|
" SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT +
|
||||||
|
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
|
||||||
|
|
||||||
|
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||||
|
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
package org.schabi.newpipe.database.history.dao;
|
|
||||||
|
|
||||||
import android.arch.persistence.room.Dao;
|
|
||||||
import android.arch.persistence.room.Query;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import io.reactivex.Flowable;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE;
|
|
||||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID;
|
|
||||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID;
|
|
||||||
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME;
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
public interface WatchHistoryDAO extends HistoryDAO<WatchHistoryEntry> {
|
|
||||||
|
|
||||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
|
||||||
@Override
|
|
||||||
WatchHistoryEntry getLatestEntry();
|
|
||||||
|
|
||||||
@Query("DELETE FROM " + TABLE_NAME)
|
|
||||||
@Override
|
|
||||||
int deleteAll();
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
|
|
||||||
@Override
|
|
||||||
Flowable<List<WatchHistoryEntry>> getAll();
|
|
||||||
|
|
||||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
|
||||||
@Override
|
|
||||||
Flowable<List<WatchHistoryEntry>> listByService(int serviceId);
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package org.schabi.newpipe.database.history.model;
|
|
||||||
|
|
||||||
import android.arch.persistence.room.ColumnInfo;
|
|
||||||
import android.arch.persistence.room.Entity;
|
|
||||||
import android.arch.persistence.room.Ignore;
|
|
||||||
import android.arch.persistence.room.PrimaryKey;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
public abstract class HistoryEntry {
|
|
||||||
|
|
||||||
public static final String ID = "id";
|
|
||||||
public static final String SERVICE_ID = "service_id";
|
|
||||||
public static final String CREATION_DATE = "creation_date";
|
|
||||||
|
|
||||||
@ColumnInfo(name = CREATION_DATE)
|
|
||||||
private Date creationDate;
|
|
||||||
|
|
||||||
@ColumnInfo(name = SERVICE_ID)
|
|
||||||
private int serviceId;
|
|
||||||
|
|
||||||
@ColumnInfo(name = ID)
|
|
||||||
@PrimaryKey(autoGenerate = true)
|
|
||||||
private long id;
|
|
||||||
|
|
||||||
public HistoryEntry(Date creationDate, int serviceId) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.creationDate = creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getCreationDate() {
|
|
||||||
return creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreationDate(Date creationDate) {
|
|
||||||
this.creationDate = creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getServiceId() {
|
|
||||||
return serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setServiceId(int serviceId) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
|
||||||
return otherEntry != null && getServiceId() == otherEntry.getServiceId();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,23 +3,66 @@ package org.schabi.newpipe.database.history.model;
|
||||||
import android.arch.persistence.room.ColumnInfo;
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
import android.arch.persistence.room.Entity;
|
import android.arch.persistence.room.Entity;
|
||||||
import android.arch.persistence.room.Ignore;
|
import android.arch.persistence.room.Ignore;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME)
|
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||||
public class SearchHistoryEntry extends HistoryEntry {
|
|
||||||
|
|
||||||
|
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||||
|
indices = {@Index(value = SEARCH)})
|
||||||
|
public class SearchHistoryEntry {
|
||||||
|
|
||||||
|
public static final String ID = "id";
|
||||||
public static final String TABLE_NAME = "search_history";
|
public static final String TABLE_NAME = "search_history";
|
||||||
|
public static final String SERVICE_ID = "service_id";
|
||||||
|
public static final String CREATION_DATE = "creation_date";
|
||||||
public static final String SEARCH = "search";
|
public static final String SEARCH = "search";
|
||||||
|
|
||||||
|
@ColumnInfo(name = ID)
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@ColumnInfo(name = CREATION_DATE)
|
||||||
|
private Date creationDate;
|
||||||
|
|
||||||
|
@ColumnInfo(name = SERVICE_ID)
|
||||||
|
private int serviceId;
|
||||||
|
|
||||||
@ColumnInfo(name = SEARCH)
|
@ColumnInfo(name = SEARCH)
|
||||||
private String search;
|
private String search;
|
||||||
|
|
||||||
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
||||||
super(creationDate, serviceId);
|
this.serviceId = serviceId;
|
||||||
|
this.creationDate = creationDate;
|
||||||
this.search = search;
|
this.search = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreationDate() {
|
||||||
|
return creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreationDate(Date creationDate) {
|
||||||
|
this.creationDate = creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getServiceId() {
|
||||||
|
return serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServiceId(int serviceId) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSearch() {
|
public String getSearch() {
|
||||||
return search;
|
return search;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +72,8 @@ public class SearchHistoryEntry extends HistoryEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@Override
|
public boolean hasEqualValues(SearchHistoryEntry otherEntry) {
|
||||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
return getServiceId() == otherEntry.getServiceId() &&
|
||||||
return otherEntry instanceof SearchHistoryEntry && super.hasEqualValues(otherEntry)
|
getSearch().equals(otherEntry.getSearch());
|
||||||
&& getSearch().equals(((SearchHistoryEntry) otherEntry).getSearch());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.schabi.newpipe.database.history.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.ForeignKey;
|
||||||
|
import android.arch.persistence.room.Ignore;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||||
|
|
||||||
|
@Entity(tableName = STREAM_HISTORY_TABLE,
|
||||||
|
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
||||||
|
// No need to index for timestamp as they will almost always be unique
|
||||||
|
indices = {@Index(value = {JOIN_STREAM_ID})},
|
||||||
|
foreignKeys = {
|
||||||
|
@ForeignKey(entity = StreamEntity.class,
|
||||||
|
parentColumns = StreamEntity.STREAM_ID,
|
||||||
|
childColumns = JOIN_STREAM_ID,
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE)
|
||||||
|
})
|
||||||
|
public class StreamHistoryEntity {
|
||||||
|
final public static String STREAM_HISTORY_TABLE = "stream_history";
|
||||||
|
final public static String JOIN_STREAM_ID = "stream_id";
|
||||||
|
final public static String STREAM_ACCESS_DATE = "access_date";
|
||||||
|
final public static String STREAM_REPEAT_COUNT = "repeat_count";
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
|
private long streamUid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||||
|
private Date accessDate;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||||
|
private long repeatCount;
|
||||||
|
|
||||||
|
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
this.accessDate = accessDate;
|
||||||
|
this.repeatCount = repeatCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
|
||||||
|
this(streamUid, accessDate, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStreamUid() {
|
||||||
|
return streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamUid(long streamUid) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getAccessDate() {
|
||||||
|
return accessDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessDate(@NonNull Date accessDate) {
|
||||||
|
this.accessDate = accessDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRepeatCount() {
|
||||||
|
return repeatCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRepeatCount(long repeatCount) {
|
||||||
|
this.repeatCount = repeatCount;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.schabi.newpipe.database.history.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class StreamHistoryEntry {
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||||
|
final public long uid;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||||
|
final public int serviceId;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||||
|
final public String url;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||||
|
final public String title;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||||
|
final public StreamType streamType;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||||
|
final public long duration;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||||
|
final public String uploader;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||||
|
final public String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
final public long streamId;
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||||
|
final public Date accessDate;
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||||
|
final public long repeatCount;
|
||||||
|
|
||||||
|
public StreamHistoryEntry(long uid, int serviceId, String url, String title,
|
||||||
|
StreamType streamType, long duration, String uploader,
|
||||||
|
String thumbnailUrl, long streamId, Date accessDate,
|
||||||
|
long repeatCount) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.url = url;
|
||||||
|
this.title = title;
|
||||||
|
this.streamType = streamType;
|
||||||
|
this.duration = duration;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.streamId = streamId;
|
||||||
|
this.accessDate = accessDate;
|
||||||
|
this.repeatCount = repeatCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamHistoryEntity toStreamHistoryEntity() {
|
||||||
|
return new StreamHistoryEntity(streamId, accessDate, repeatCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasEqualValues(StreamHistoryEntry other) {
|
||||||
|
return this.uid == other.uid && streamId == other.streamId &&
|
||||||
|
accessDate.compareTo(other.accessDate) == 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,109 +0,0 @@
|
||||||
package org.schabi.newpipe.database.history.model;
|
|
||||||
|
|
||||||
import android.arch.persistence.room.ColumnInfo;
|
|
||||||
import android.arch.persistence.room.Entity;
|
|
||||||
import android.arch.persistence.room.Ignore;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Entity(tableName = WatchHistoryEntry.TABLE_NAME)
|
|
||||||
public class WatchHistoryEntry extends HistoryEntry {
|
|
||||||
|
|
||||||
public static final String TABLE_NAME = "watch_history";
|
|
||||||
public static final String TITLE = "title";
|
|
||||||
public static final String URL = "url";
|
|
||||||
public static final String STREAM_ID = "stream_id";
|
|
||||||
public static final String THUMBNAIL_URL = "thumbnail_url";
|
|
||||||
public static final String UPLOADER = "uploader";
|
|
||||||
public static final String DURATION = "duration";
|
|
||||||
|
|
||||||
@ColumnInfo(name = TITLE)
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
@ColumnInfo(name = URL)
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
@ColumnInfo(name = STREAM_ID)
|
|
||||||
private String streamId;
|
|
||||||
|
|
||||||
@ColumnInfo(name = THUMBNAIL_URL)
|
|
||||||
private String thumbnailURL;
|
|
||||||
|
|
||||||
@ColumnInfo(name = UPLOADER)
|
|
||||||
private String uploader;
|
|
||||||
|
|
||||||
@ColumnInfo(name = DURATION)
|
|
||||||
private long duration;
|
|
||||||
|
|
||||||
public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) {
|
|
||||||
super(creationDate, serviceId);
|
|
||||||
this.title = title;
|
|
||||||
this.url = url;
|
|
||||||
this.streamId = streamId;
|
|
||||||
this.thumbnailURL = thumbnailURL;
|
|
||||||
this.uploader = uploader;
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WatchHistoryEntry(StreamInfo streamInfo) {
|
|
||||||
this(new Date(), streamInfo.getServiceId(), streamInfo.getName(), streamInfo.getUrl(),
|
|
||||||
streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStreamId() {
|
|
||||||
return streamId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreamId(String streamId) {
|
|
||||||
this.streamId = streamId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getThumbnailURL() {
|
|
||||||
return thumbnailURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThumbnailURL(String thumbnailURL) {
|
|
||||||
this.thumbnailURL = thumbnailURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUploader() {
|
|
||||||
return uploader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUploader(String uploader) {
|
|
||||||
this.uploader = uploader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDuration() {
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDuration(int duration) {
|
|
||||||
this.duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Override
|
|
||||||
public boolean hasEqualValues(HistoryEntry otherEntry) {
|
|
||||||
return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry)
|
|
||||||
&& getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
|
||||||
|
public interface PlaylistLocalItem extends LocalItem {
|
||||||
|
String getOrderingName();
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
|
||||||
|
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_THUMBNAIL_URL;
|
||||||
|
|
||||||
|
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||||
|
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
|
final public long uid;
|
||||||
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
|
final public String name;
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
|
final public String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
|
final public long streamCount;
|
||||||
|
|
||||||
|
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.name = name;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalItemType getLocalItemType() {
|
||||||
|
return LocalItemType.PLAYLIST_LOCAL_ITEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOrderingName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.schabi.newpipe.database.playlist;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
public class PlaylistStreamEntry implements LocalItem {
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||||
|
final public long uid;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||||
|
final public int serviceId;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||||
|
final public String url;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||||
|
final public String title;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||||
|
final public StreamType streamType;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||||
|
final public long duration;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||||
|
final public String uploader;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||||
|
final public String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||||
|
final public long streamId;
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||||
|
final public int joinIndex;
|
||||||
|
|
||||||
|
public PlaylistStreamEntry(long uid, int serviceId, String url, String title,
|
||||||
|
StreamType streamType, long duration, String uploader,
|
||||||
|
String thumbnailUrl, long streamId, int joinIndex) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.url = url;
|
||||||
|
this.title = title;
|
||||||
|
this.streamType = streamType;
|
||||||
|
this.duration = duration;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.streamId = streamId;
|
||||||
|
this.joinIndex = joinIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamInfoItem toStreamInfoItem() throws IllegalArgumentException {
|
||||||
|
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
||||||
|
item.setThumbnailUrl(thumbnailUrl);
|
||||||
|
item.setUploaderName(uploader);
|
||||||
|
item.setDuration(duration);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalItemType getLocalItemType() {
|
||||||
|
return LocalItemType.PLAYLIST_STREAM_ITEM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + PLAYLIST_TABLE)
|
||||||
|
public abstract Flowable<List<PlaylistEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + PLAYLIST_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
|
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||||
|
public abstract int deletePlaylist(final long playlistId);
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
|
||||||
|
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
|
||||||
|
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " +
|
||||||
|
REMOTE_PLAYLIST_URL + " = :url AND " +
|
||||||
|
REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE +
|
||||||
|
" WHERE " +
|
||||||
|
REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||||
|
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public long upsert(PlaylistRemoteEntity playlist) {
|
||||||
|
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||||
|
|
||||||
|
if (playlistId == null) {
|
||||||
|
return insert(playlist);
|
||||||
|
} else {
|
||||||
|
playlist.setUid(playlistId);
|
||||||
|
update(playlist);
|
||||||
|
return playlistId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
|
||||||
|
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||||
|
public abstract int deletePlaylist(final long playlistId);
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.dao;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||||
|
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||||
|
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
|
public abstract void deleteBatch(final long playlistId);
|
||||||
|
|
||||||
|
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
|
||||||
|
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||||
|
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||||
|
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
|
||||||
|
// get ids of streams of the given playlist
|
||||||
|
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
|
||||||
|
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||||
|
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" +
|
||||||
|
|
||||||
|
// then merge with the stream metadata
|
||||||
|
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||||
|
" ORDER BY " + JOIN_INDEX + " ASC")
|
||||||
|
public abstract 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 +
|
||||||
|
|
||||||
|
" FROM " + PLAYLIST_TABLE +
|
||||||
|
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||||
|
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
|
||||||
|
" GROUP BY " + JOIN_PLAYLIST_ID +
|
||||||
|
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||||
|
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||||
|
|
||||||
|
@Entity(tableName = PLAYLIST_TABLE,
|
||||||
|
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||||
|
public class PlaylistEntity {
|
||||||
|
final public static String PLAYLIST_TABLE = "playlists";
|
||||||
|
final public static String PLAYLIST_ID = "uid";
|
||||||
|
final public static String PLAYLIST_NAME = "name";
|
||||||
|
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
|
private long uid = 0;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||||
|
private String thumbnailUrl;
|
||||||
|
|
||||||
|
public PlaylistEntity(String name, String thumbnailUrl) {
|
||||||
|
this.name = name;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(long uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailUrl(String thumbnailUrl) {
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.Ignore;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
|
||||||
|
|
||||||
|
@Entity(tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
|
indices = {
|
||||||
|
@Index(value = {REMOTE_PLAYLIST_NAME}),
|
||||||
|
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||||
|
})
|
||||||
|
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||||
|
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||||
|
final public static String REMOTE_PLAYLIST_ID = "uid";
|
||||||
|
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||||
|
final public static String REMOTE_PLAYLIST_NAME = "name";
|
||||||
|
final public static String REMOTE_PLAYLIST_URL = "url";
|
||||||
|
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||||
|
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||||
|
private long uid = 0;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
|
||||||
|
private int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
|
||||||
|
private String thumbnailUrl;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
|
private String uploader;
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
|
private Long streamCount;
|
||||||
|
|
||||||
|
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
|
||||||
|
String uploader, Long streamCount) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||||
|
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||||
|
info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||||
|
info.getUploaderName(), info.getStreamCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(long uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getServiceId() {
|
||||||
|
return serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServiceId(int serviceId) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailUrl(String thumbnailUrl) {
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUploader() {
|
||||||
|
return uploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploader(String uploader) {
|
||||||
|
this.uploader = uploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getStreamCount() {
|
||||||
|
return streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamCount(Long streamCount) {
|
||||||
|
this.streamCount = streamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalItemType getLocalItemType() {
|
||||||
|
return PLAYLIST_REMOTE_ITEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOrderingName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package org.schabi.newpipe.database.playlist.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.ForeignKey;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
|
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||||
|
|
||||||
|
@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE,
|
||||||
|
primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX},
|
||||||
|
indices = {
|
||||||
|
@Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true),
|
||||||
|
@Index(value = {JOIN_STREAM_ID})
|
||||||
|
},
|
||||||
|
foreignKeys = {
|
||||||
|
@ForeignKey(entity = PlaylistEntity.class,
|
||||||
|
parentColumns = PlaylistEntity.PLAYLIST_ID,
|
||||||
|
childColumns = JOIN_PLAYLIST_ID,
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||||
|
@ForeignKey(entity = StreamEntity.class,
|
||||||
|
parentColumns = StreamEntity.STREAM_ID,
|
||||||
|
childColumns = JOIN_STREAM_ID,
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||||
|
})
|
||||||
|
public class PlaylistStreamEntity {
|
||||||
|
|
||||||
|
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||||
|
final public static String JOIN_PLAYLIST_ID = "playlist_id";
|
||||||
|
final public static String JOIN_STREAM_ID = "stream_id";
|
||||||
|
final public static String JOIN_INDEX = "join_index";
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||||
|
private long playlistUid;
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
|
private long streamUid;
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_INDEX)
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) {
|
||||||
|
this.playlistUid = playlistUid;
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getPlaylistUid() {
|
||||||
|
return playlistUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStreamUid() {
|
||||||
|
return streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylistUid(long playlistUid) {
|
||||||
|
this.playlistUid = playlistUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamUid(long streamUid) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndex(int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.schabi.newpipe.database.stream;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
|
||||||
|
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.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class StreamStatisticsEntry implements LocalItem {
|
||||||
|
final public static String STREAM_LATEST_DATE = "latestAccess";
|
||||||
|
final public static String STREAM_WATCH_COUNT = "watchCount";
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_ID)
|
||||||
|
final public long uid;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_SERVICE_ID)
|
||||||
|
final public int serviceId;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_URL)
|
||||||
|
final public String url;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TITLE)
|
||||||
|
final public String title;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||||
|
final public StreamType streamType;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||||
|
final public long duration;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_UPLOADER)
|
||||||
|
final public String uploader;
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_THUMBNAIL_URL)
|
||||||
|
final public String thumbnailUrl;
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
final public long streamId;
|
||||||
|
@ColumnInfo(name = StreamStatisticsEntry.STREAM_LATEST_DATE)
|
||||||
|
final public Date latestAccessDate;
|
||||||
|
@ColumnInfo(name = StreamStatisticsEntry.STREAM_WATCH_COUNT)
|
||||||
|
final public long watchCount;
|
||||||
|
|
||||||
|
public StreamStatisticsEntry(long uid, int serviceId, String url, String title,
|
||||||
|
StreamType streamType, long duration, String uploader,
|
||||||
|
String thumbnailUrl, long streamId, Date latestAccessDate,
|
||||||
|
long watchCount) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.url = url;
|
||||||
|
this.title = title;
|
||||||
|
this.streamType = streamType;
|
||||||
|
this.duration = duration;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.streamId = streamId;
|
||||||
|
this.latestAccessDate = latestAccessDate;
|
||||||
|
this.watchCount = watchCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamInfoItem toStreamInfoItem() {
|
||||||
|
StreamInfoItem item = new StreamInfoItem(serviceId, url, title, streamType);
|
||||||
|
item.setDuration(duration);
|
||||||
|
item.setUploaderName(uploader);
|
||||||
|
item.setThumbnailUrl(thumbnailUrl);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LocalItemType getLocalItemType() {
|
||||||
|
return LocalItemType.STATISTIC_STREAM_ITEM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package org.schabi.newpipe.database.stream.dao;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Insert;
|
||||||
|
import android.arch.persistence.room.OnConflictStrategy;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||||
|
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class StreamDAO implements BasicDAO<StreamEntity> {
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE)
|
||||||
|
public abstract Flowable<List<StreamEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + STREAM_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " + STREAM_SERVICE_ID + " = :serviceId")
|
||||||
|
public abstract Flowable<List<StreamEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_TABLE + " WHERE " +
|
||||||
|
STREAM_URL + " = :url AND " +
|
||||||
|
STREAM_SERVICE_ID + " = :serviceId")
|
||||||
|
public abstract Flowable<List<StreamEntity>> getStream(long serviceId, String url);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract void silentInsertAllInternal(final List<StreamEntity> streams);
|
||||||
|
|
||||||
|
@Query("SELECT " + STREAM_ID + " FROM " + STREAM_TABLE + " WHERE " +
|
||||||
|
STREAM_URL + " = :url AND " +
|
||||||
|
STREAM_SERVICE_ID + " = :serviceId")
|
||||||
|
abstract Long getStreamIdInternal(long serviceId, String url);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public long upsert(StreamEntity stream) {
|
||||||
|
final Long streamIdCandidate = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
||||||
|
|
||||||
|
if (streamIdCandidate == null) {
|
||||||
|
return insert(stream);
|
||||||
|
} else {
|
||||||
|
stream.setUid(streamIdCandidate);
|
||||||
|
update(stream);
|
||||||
|
return streamIdCandidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public List<Long> upsertAll(List<StreamEntity> streams) {
|
||||||
|
silentInsertAllInternal(streams);
|
||||||
|
|
||||||
|
final List<Long> streamIds = new ArrayList<>(streams.size());
|
||||||
|
for (StreamEntity stream : streams) {
|
||||||
|
final Long streamId = getStreamIdInternal(stream.getServiceId(), stream.getUrl());
|
||||||
|
if (streamId == null) {
|
||||||
|
throw new IllegalStateException("StreamID cannot be null just after insertion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
streamIds.add(streamId);
|
||||||
|
stream.setUid(streamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(streams);
|
||||||
|
return streamIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + STREAM_TABLE + " WHERE " + STREAM_ID +
|
||||||
|
" NOT IN " +
|
||||||
|
"(SELECT DISTINCT " + STREAM_ID + " FROM " + STREAM_TABLE +
|
||||||
|
|
||||||
|
" LEFT JOIN " + STREAM_HISTORY_TABLE +
|
||||||
|
" ON " + STREAM_ID + " = " +
|
||||||
|
StreamHistoryEntity.STREAM_HISTORY_TABLE + "." + StreamHistoryEntity.JOIN_STREAM_ID +
|
||||||
|
|
||||||
|
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||||
|
" ON " + STREAM_ID + " = " +
|
||||||
|
PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE + "." + PlaylistStreamEntity.JOIN_STREAM_ID +
|
||||||
|
")")
|
||||||
|
public abstract int deleteOrphans();
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.schabi.newpipe.database.stream.dao;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.Dao;
|
||||||
|
import android.arch.persistence.room.Insert;
|
||||||
|
import android.arch.persistence.room.OnConflictStrategy;
|
||||||
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
|
||||||
|
@Override
|
||||||
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
|
||||||
|
public abstract Flowable<List<StreamStateEntity>> getAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Query("DELETE FROM " + STREAM_STATE_TABLE)
|
||||||
|
public abstract int deleteAll();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
|
public abstract Flowable<List<StreamStateEntity>> getState(final long streamId);
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||||
|
public abstract int deleteState(final long streamId);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract void silentInsertInternal(final StreamStateEntity streamState);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public long upsert(StreamStateEntity stream) {
|
||||||
|
silentInsertInternal(stream);
|
||||||
|
return update(stream);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package org.schabi.newpipe.database.stream.model;
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.Ignore;
|
||||||
|
import android.arch.persistence.room.Index;
|
||||||
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_SERVICE_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
|
||||||
|
|
||||||
|
@Entity(tableName = STREAM_TABLE,
|
||||||
|
indices = {@Index(value = {STREAM_SERVICE_ID, STREAM_URL}, unique = true)})
|
||||||
|
public class StreamEntity implements Serializable {
|
||||||
|
|
||||||
|
final public static String STREAM_TABLE = "streams";
|
||||||
|
final public static String STREAM_ID = "uid";
|
||||||
|
final public static String STREAM_SERVICE_ID = "service_id";
|
||||||
|
final public static String STREAM_URL = "url";
|
||||||
|
final public static String STREAM_TITLE = "title";
|
||||||
|
final public static String STREAM_TYPE = "stream_type";
|
||||||
|
final public static String STREAM_DURATION = "duration";
|
||||||
|
final public static String STREAM_UPLOADER = "uploader";
|
||||||
|
final public static String STREAM_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
private long uid = 0;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||||
|
private int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_URL)
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TITLE)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TYPE)
|
||||||
|
private StreamType streamType;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_DURATION)
|
||||||
|
private Long duration;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOADER)
|
||||||
|
private String uploader;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||||
|
private String thumbnailUrl;
|
||||||
|
|
||||||
|
public StreamEntity(final int serviceId, final String title, final String url,
|
||||||
|
final StreamType streamType, final String thumbnailUrl, final String uploader,
|
||||||
|
final long duration) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.title = title;
|
||||||
|
this.url = url;
|
||||||
|
this.streamType = streamType;
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public StreamEntity(final StreamInfoItem item) {
|
||||||
|
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
|
||||||
|
item.uploader_name, item.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public StreamEntity(final StreamInfo info) {
|
||||||
|
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
|
||||||
|
info.uploader_name, info.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public StreamEntity(final PlayQueueItem item) {
|
||||||
|
this(item.getServiceId(), item.getTitle(), item.getUrl(), item.getStreamType(),
|
||||||
|
item.getThumbnailUrl(), item.getUploader(), item.getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(long uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getServiceId() {
|
||||||
|
return serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServiceId(int serviceId) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return streamType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamType(StreamType type) {
|
||||||
|
this.streamType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDuration(Long duration) {
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUploader() {
|
||||||
|
return uploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUploader(String uploader) {
|
||||||
|
this.uploader = uploader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getThumbnailUrl() {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setThumbnailUrl(String thumbnailUrl) {
|
||||||
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.schabi.newpipe.database.stream.model;
|
||||||
|
|
||||||
|
|
||||||
|
import android.arch.persistence.room.ColumnInfo;
|
||||||
|
import android.arch.persistence.room.Entity;
|
||||||
|
import android.arch.persistence.room.ForeignKey;
|
||||||
|
|
||||||
|
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||||
|
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||||
|
|
||||||
|
@Entity(tableName = STREAM_STATE_TABLE,
|
||||||
|
primaryKeys = {JOIN_STREAM_ID},
|
||||||
|
foreignKeys = {
|
||||||
|
@ForeignKey(entity = StreamEntity.class,
|
||||||
|
parentColumns = StreamEntity.STREAM_ID,
|
||||||
|
childColumns = JOIN_STREAM_ID,
|
||||||
|
onDelete = CASCADE, onUpdate = CASCADE)
|
||||||
|
})
|
||||||
|
public class StreamStateEntity {
|
||||||
|
final public static String STREAM_STATE_TABLE = "stream_state";
|
||||||
|
final public static String JOIN_STREAM_ID = "stream_id";
|
||||||
|
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
|
private long streamUid;
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||||
|
private long progressTime;
|
||||||
|
|
||||||
|
public StreamStateEntity(long streamUid, long progressTime) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
this.progressTime = progressTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStreamUid() {
|
||||||
|
return streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamUid(long streamUid) {
|
||||||
|
this.streamUid = streamUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getProgressTime() {
|
||||||
|
return progressTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressTime(long progressTime) {
|
||||||
|
this.progressTime = progressTime;
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,8 +50,7 @@ public class SubscriptionEntity {
|
||||||
return uid;
|
return uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep this package-private since UID should always be auto generated by Room impl */
|
public void setUid(long uid) {
|
||||||
void setUid(long uid) {
|
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||||
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
|
import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment;
|
||||||
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
@ -84,12 +85,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
|
|
||||||
int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel);
|
int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel);
|
||||||
int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot);
|
int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot);
|
||||||
|
int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark);
|
||||||
|
|
||||||
if (isSubscriptionsPageOnlySelected()) {
|
if (isSubscriptionsPageOnlySelected()) {
|
||||||
tabLayout.getTabAt(0).setIcon(channelIcon);
|
tabLayout.getTabAt(0).setIcon(channelIcon);
|
||||||
|
tabLayout.getTabAt(1).setIcon(bookmarkIcon);
|
||||||
} else {
|
} else {
|
||||||
tabLayout.getTabAt(0).setIcon(whatsHotIcon);
|
tabLayout.getTabAt(0).setIcon(whatsHotIcon);
|
||||||
tabLayout.getTabAt(1).setIcon(channelIcon);
|
tabLayout.getTabAt(1).setIcon(channelIcon);
|
||||||
|
tabLayout.getTabAt(2).setIcon(bookmarkIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +151,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PagerAdapter extends FragmentPagerAdapter {
|
private class PagerAdapter extends FragmentPagerAdapter {
|
||||||
|
|
||||||
PagerAdapter(FragmentManager fm) {
|
PagerAdapter(FragmentManager fm) {
|
||||||
super(fm);
|
super(fm);
|
||||||
}
|
}
|
||||||
|
@ -158,7 +161,15 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
case 0:
|
case 0:
|
||||||
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment();
|
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment();
|
||||||
case 1:
|
case 1:
|
||||||
|
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||||
|
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
|
||||||
|
.equals(getString(R.string.subscription_page_key))) {
|
||||||
|
return new BookmarkFragment();
|
||||||
|
} else {
|
||||||
return new SubscriptionFragment();
|
return new SubscriptionFragment();
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return new BookmarkFragment();
|
||||||
default:
|
default:
|
||||||
return new BlankFragment();
|
return new BlankFragment();
|
||||||
}
|
}
|
||||||
|
@ -172,7 +183,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return isSubscriptionsPageOnlySelected() ? 1 : 2;
|
return isSubscriptionsPageOnlySelected() ? 2 : 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,6 +198,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
}
|
}
|
||||||
|
|
||||||
private Fragment getMainPageFragment() {
|
private Fragment getMainPageFragment() {
|
||||||
|
if (getActivity() == null) return new BlankFragment();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SharedPreferences preferences =
|
SharedPreferences preferences =
|
||||||
PreferenceManager.getDefaultSharedPreferences(getActivity());
|
PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
|
|
@ -58,7 +58,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.history.HistoryListener;
|
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.player.MainVideoPlayer;
|
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||||
|
@ -75,6 +75,7 @@ import org.schabi.newpipe.util.InfoCache;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
@ -145,6 +146,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
private TextView detailControlsBackground;
|
private TextView detailControlsBackground;
|
||||||
private TextView detailControlsPopup;
|
private TextView detailControlsPopup;
|
||||||
|
private TextView detailControlsAddToPlaylist;
|
||||||
private TextView appendControlsDetail;
|
private TextView appendControlsDetail;
|
||||||
|
|
||||||
private LinearLayout videoDescriptionRootLayout;
|
private LinearLayout videoDescriptionRootLayout;
|
||||||
|
@ -327,6 +329,12 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
case R.id.detail_controls_popup:
|
case R.id.detail_controls_popup:
|
||||||
openPopupPlayer(false);
|
openPopupPlayer(false);
|
||||||
break;
|
break;
|
||||||
|
case R.id.detail_controls_playlist_append:
|
||||||
|
if (getFragmentManager() != null && currentInfo != null) {
|
||||||
|
PlaylistAppendDialog.fromStreamInfo(currentInfo)
|
||||||
|
.show(getFragmentManager(), TAG);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case R.id.detail_uploader_root_layout:
|
case R.id.detail_uploader_root_layout:
|
||||||
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
||||||
Log.w(TAG, "Can't open channel because we got no channel URL");
|
Log.w(TAG, "Can't open channel because we got no channel URL");
|
||||||
|
@ -429,6 +437,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
|
detailControlsBackground = rootView.findViewById(R.id.detail_controls_background);
|
||||||
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
|
detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup);
|
||||||
|
detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append);
|
||||||
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
||||||
|
|
||||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||||
|
@ -462,7 +471,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
infoItemBuilder.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
|
infoItemBuilder.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(StreamInfoItem selectedItem) {
|
public void selected(StreamInfoItem selectedItem) {
|
||||||
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
selectAndLoadVideo(selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||||
|
@ -479,6 +488,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
thumbnailBackgroundButton.setOnClickListener(this);
|
thumbnailBackgroundButton.setOnClickListener(this);
|
||||||
detailControlsBackground.setOnClickListener(this);
|
detailControlsBackground.setOnClickListener(this);
|
||||||
detailControlsPopup.setOnClickListener(this);
|
detailControlsPopup.setOnClickListener(this);
|
||||||
|
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||||
relatedStreamExpandButton.setOnClickListener(this);
|
relatedStreamExpandButton.setOnClickListener(this);
|
||||||
|
|
||||||
detailControlsBackground.setLongClickable(true);
|
detailControlsBackground.setLongClickable(true);
|
||||||
|
@ -638,9 +648,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
public void onActionSelected(int selectedStreamId) {
|
public void onActionSelected(int selectedStreamId) {
|
||||||
try {
|
try {
|
||||||
NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http")));
|
NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http")));
|
||||||
if(activity instanceof HistoryListener) {
|
|
||||||
((HistoryListener) activity).onVideoPlayed(info, null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
|
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
|
||||||
showInstallKoreDialog(activity);
|
showInstallKoreDialog(activity);
|
||||||
|
@ -794,10 +801,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private void openBackgroundPlayer(final boolean append) {
|
private void openBackgroundPlayer(final boolean append) {
|
||||||
AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
AudioStream audioStream = currentInfo.getAudioStreams().get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
|
||||||
((HistoryListener) activity).onAudioPlayed(currentInfo, audioStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
|
||||||
|
|
||||||
|
@ -814,10 +817,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
|
||||||
((HistoryListener) activity).onVideoPlayed(currentInfo, getSelectedVideoStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
final PlayQueue itemQueue = new SinglePlayQueue(currentInfo);
|
||||||
if (append) {
|
if (append) {
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
|
NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue);
|
||||||
|
@ -833,10 +832,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private void openVideoPlayer() {
|
private void openVideoPlayer() {
|
||||||
VideoStream selectedVideoStream = getSelectedVideoStream();
|
VideoStream selectedVideoStream = getSelectedVideoStream();
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
|
||||||
((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||||
NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream);
|
NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1033,6 +1028,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (!TextUtils.isEmpty(info.getUploaderName())) {
|
if (!TextUtils.isEmpty(info.getUploaderName())) {
|
||||||
uploaderTextView.setText(info.getUploaderName());
|
uploaderTextView.setText(info.getUploaderName());
|
||||||
uploaderTextView.setVisibility(View.VISIBLE);
|
uploaderTextView.setVisibility(View.VISIBLE);
|
||||||
|
uploaderTextView.setSelected(true);
|
||||||
} else {
|
} else {
|
||||||
uploaderTextView.setVisibility(View.GONE);
|
uploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,15 @@ package org.schabi.newpipe.fragments.list;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
@ -24,14 +20,15 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
|
||||||
|
@ -140,7 +137,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
@Override
|
@Override
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
|
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(StreamInfoItem selectedItem) {
|
public void selected(StreamInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
|
@ -155,7 +152,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
|
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(ChannelInfoItem selectedItem) {
|
public void selected(ChannelInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
|
@ -163,12 +160,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void held(ChannelInfoItem selectedItem) {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
|
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(PlaylistInfoItem selectedItem) {
|
public void selected(PlaylistInfoItem selectedItem) {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
|
@ -176,9 +170,6 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void held(PlaylistInfoItem selectedItem) {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
itemsList.clearOnScrollListeners();
|
itemsList.clearOnScrollListeners();
|
||||||
|
@ -203,12 +194,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
|
|
||||||
final String[] commands = new String[]{
|
final String[] commands = new String[]{
|
||||||
context.getResources().getString(R.string.enqueue_on_background),
|
context.getResources().getString(R.string.enqueue_on_background),
|
||||||
context.getResources().getString(R.string.enqueue_on_popup)
|
context.getResources().getString(R.string.enqueue_on_popup),
|
||||||
|
context.getResources().getString(R.string.append_playlist)
|
||||||
};
|
};
|
||||||
|
|
||||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialogInterface, int i) {
|
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||||
|
@ -216,10 +206,15 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
case 1:
|
case 1:
|
||||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||||
break;
|
break;
|
||||||
|
case 2:
|
||||||
|
if (getFragmentManager() != null) {
|
||||||
|
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||||
|
.show(getFragmentManager(), TAG);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||||
|
|
|
@ -194,17 +194,14 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if(useAsFrontPage) {
|
if(useAsFrontPage && supportActionBar != null) {
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
inflater.inflate(R.menu.menu_channel, menu);
|
inflater.inflate(R.menu.menu_channel, menu);
|
||||||
|
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||||
|
"], inflater = [" + inflater + "]");
|
||||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||||
if (currentInfo != null) {
|
|
||||||
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.getFeedUrl()));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,10 +222,9 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
openUrlInBrowser(url);
|
openUrlInBrowser(url);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share: {
|
case R.id.menu_item_share:
|
||||||
shareUrl(name, url);
|
shareUrl(name, url);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
@ -428,6 +424,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||||
|
|
||||||
playlistCtrl.setVisibility(View.VISIBLE);
|
playlistCtrl.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
if (!result.errors.isEmpty()) {
|
if (!result.errors.isEmpty()) {
|
||||||
|
|
|
@ -17,13 +17,18 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscriber;
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
|
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||||
|
@ -31,13 +36,27 @@ import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.disposables.Disposables;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
|
|
||||||
|
private CompositeDisposable disposables;
|
||||||
|
private Subscription bookmarkReactor;
|
||||||
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
|
|
||||||
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
|
private PlaylistRemoteEntity playlistEntity;
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -54,6 +73,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
private View headerPopupButton;
|
private View headerPopupButton;
|
||||||
private View headerBackgroundButton;
|
private View headerBackgroundButton;
|
||||||
|
|
||||||
|
private MenuItem playlistBookmarkButton;
|
||||||
|
|
||||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
||||||
PlaylistFragment instance = new PlaylistFragment();
|
PlaylistFragment instance = new PlaylistFragment();
|
||||||
instance.setInitialData(serviceId, url, name);
|
instance.setInitialData(serviceId, url, name);
|
||||||
|
@ -65,7 +86,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
disposables = new CompositeDisposable();
|
||||||
|
isBookmarkButtonReady = new AtomicBoolean(false);
|
||||||
|
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +116,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||||
|
|
||||||
|
|
||||||
return headerRootLayout;
|
return headerRootLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +141,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
context.getResources().getString(R.string.start_here_on_popup),
|
context.getResources().getString(R.string.start_here_on_popup),
|
||||||
};
|
};
|
||||||
|
|
||||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialogInterface, int i) {
|
|
||||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -133,7 +162,6 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||||
|
@ -141,9 +169,36 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||||
|
"], inflater = [" + inflater + "]");
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
inflater.inflate(R.menu.menu_playlist, menu);
|
inflater.inflate(R.menu.menu_playlist, menu);
|
||||||
|
|
||||||
|
playlistBookmarkButton = menu.findItem(R.id.menu_item_bookmark);
|
||||||
|
updateBookmarkButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false);
|
||||||
|
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||||
|
|
||||||
|
bookmarkReactor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (disposables != null) disposables.dispose();
|
||||||
|
|
||||||
|
disposables = null;
|
||||||
|
remotePlaylistManager = null;
|
||||||
|
playlistEntity = null;
|
||||||
|
isBookmarkButtonReady = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -166,10 +221,12 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
case R.id.menu_item_openInBrowser:
|
case R.id.menu_item_openInBrowser:
|
||||||
openUrlInBrowser(url);
|
openUrlInBrowser(url);
|
||||||
break;
|
break;
|
||||||
case R.id.menu_item_share: {
|
case R.id.menu_item_share:
|
||||||
shareUrl(name, url);
|
shareUrl(name, url);
|
||||||
break;
|
break;
|
||||||
}
|
case R.id.menu_item_bookmark:
|
||||||
|
onBookmarkClicked();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
@ -201,12 +258,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||||
headerUploaderName.setText(result.getUploaderName());
|
headerUploaderName.setText(result.getUploaderName());
|
||||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||||
headerUploaderLayout.setOnClickListener(new View.OnClickListener() {
|
headerUploaderLayout.setOnClickListener(v ->
|
||||||
@Override
|
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||||
public void onClick(View v) {
|
result.getServiceId(), result.getUploaderUrl(),
|
||||||
NavigationHelper.openChannelFragment(getFragmentManager(), result.getServiceId(), result.getUploaderUrl(), result.getUploaderName());
|
result.getUploaderName())
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,24 +275,21 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
|
remotePlaylistManager.getPlaylist(result)
|
||||||
@Override
|
.onBackpressureLatest()
|
||||||
public void onClick(View view) {
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
.subscribe(getPlaylistBookmarkSubscriber());
|
||||||
}
|
|
||||||
});
|
remotePlaylistManager.onUpdate(result)
|
||||||
headerPopupButton.setOnClickListener(new View.OnClickListener() {
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
@Override
|
.subscribe(integer -> {/* Do nothing*/}, this::onError);
|
||||||
public void onClick(View view) {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
}
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||||
});
|
headerPopupButton.setOnClickListener(view ->
|
||||||
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||||
@Override
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
public void onClick(View view) {
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
private PlayQueue getPlayQueue() {
|
||||||
|
@ -280,9 +333,76 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||||
|
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription s) {
|
||||||
|
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||||
|
bookmarkReactor = s;
|
||||||
|
bookmarkReactor.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(List<PlaylistRemoteEntity> playlist) {
|
||||||
|
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
|
||||||
|
|
||||||
|
updateBookmarkButtons();
|
||||||
|
isBookmarkButtonReady.set(true);
|
||||||
|
|
||||||
|
if (bookmarkReactor != null) bookmarkReactor.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable t) {
|
||||||
|
PlaylistFragment.this.onError(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(String title) {
|
||||||
super.setTitle(title);
|
super.setTitle(title);
|
||||||
headerTitleView.setText(title);
|
headerTitleView.setText(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onBookmarkClicked() {
|
||||||
|
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() ||
|
||||||
|
remotePlaylistManager == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
final Disposable action;
|
||||||
|
|
||||||
|
if (currentInfo != null && playlistEntity == null) {
|
||||||
|
action = remotePlaylistManager.onBookmark(currentInfo)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||||
|
} else if (playlistEntity != null) {
|
||||||
|
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doFinally(() -> playlistEntity = null)
|
||||||
|
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||||
|
} else {
|
||||||
|
action = Disposables.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
disposables.add(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBookmarkButtons() {
|
||||||
|
if (playlistBookmarkButton == null || activity == null) return;
|
||||||
|
|
||||||
|
final int iconAttr = playlistEntity == null ?
|
||||||
|
R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
||||||
|
|
||||||
|
final int titleRes = playlistEntity == null ?
|
||||||
|
R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||||
|
|
||||||
|
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||||
|
playlistBookmarkButton.setTitle(titleRes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.fragments.list.search;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -30,10 +29,8 @@ import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.ReCaptchaActivity;
|
import org.schabi.newpipe.ReCaptchaActivity;
|
||||||
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.ListExtractor;
|
import org.schabi.newpipe.extractor.ListExtractor;
|
||||||
|
@ -44,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.history.HistoryListener;
|
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
|
@ -64,16 +61,11 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Flowable;
|
import io.reactivex.Flowable;
|
||||||
import io.reactivex.Notification;
|
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.ObservableSource;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.BiFunction;
|
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.functions.Function;
|
|
||||||
import io.reactivex.functions.Predicate;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import io.reactivex.subjects.PublishSubject;
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
|
@ -121,7 +113,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private SuggestionListAdapter suggestionListAdapter;
|
private SuggestionListAdapter suggestionListAdapter;
|
||||||
private SearchHistoryDAO searchHistoryDAO;
|
private HistoryRecordManager historyRecordManager;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
|
@ -167,7 +159,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
|
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
|
||||||
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
|
||||||
|
|
||||||
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO();
|
historyRecordManager = new HistoryRecordManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -535,36 +527,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||||
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
howManyDeleted -> suggestionPublisher
|
||||||
|
.onNext(searchEditText.getText().toString()),
|
||||||
|
|
||||||
|
throwable -> showSnackBarError(throwable,
|
||||||
|
UserAction.SOMETHING_ELSE, "none",
|
||||||
|
"Deleting item failed", R.string.general_error)
|
||||||
|
);
|
||||||
|
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(item.query)
|
.setTitle(item.query)
|
||||||
.setMessage(R.string.delete_item_search_history)
|
.setMessage(R.string.delete_item_search_history)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
|
||||||
@Override
|
.show();
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
disposables.add(Observable
|
|
||||||
.fromCallable(new Callable<Integer>() {
|
|
||||||
@Override
|
|
||||||
public Integer call() throws Exception {
|
|
||||||
return searchHistoryDAO.deleteAllWhereQuery(item.query);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(new Consumer<Integer>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Integer howManyDeleted) throws Exception {
|
|
||||||
suggestionPublisher.onNext(searchEditText.getText().toString());
|
|
||||||
}
|
|
||||||
}, new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Throwable throwable) throws Exception {
|
|
||||||
showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -589,29 +569,18 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
final Observable<String> observable = suggestionPublisher
|
final Observable<String> observable = suggestionPublisher
|
||||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||||
.startWith(searchQuery != null ? searchQuery : "")
|
.startWith(searchQuery != null ? searchQuery : "")
|
||||||
.filter(new Predicate<String>() {
|
.filter(query -> isSuggestionsEnabled);
|
||||||
@Override
|
|
||||||
public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception {
|
|
||||||
return isSuggestionsEnabled;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
suggestionDisposable = observable
|
suggestionDisposable = observable
|
||||||
.switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() {
|
.switchMap(query -> {
|
||||||
@Override
|
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
|
||||||
public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception {
|
.getRelatedSearches(query, 3, 25);
|
||||||
final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0
|
|
||||||
? searchHistoryDAO.getSimilarEntries(query, 3)
|
|
||||||
: searchHistoryDAO.getUniqueEntries(25);
|
|
||||||
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
final Observable<List<SuggestionItem>> local = flowable.toObservable()
|
||||||
.map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() {
|
.map(searchHistoryEntries -> {
|
||||||
@Override
|
|
||||||
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SearchHistoryEntry> searchHistoryEntries) throws Exception {
|
|
||||||
List<SuggestionItem> result = new ArrayList<>();
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
for (SearchHistoryEntry entry : searchHistoryEntries)
|
for (SearchHistoryEntry entry : searchHistoryEntries)
|
||||||
result.add(new SuggestionItem(true, entry.getSearch()));
|
result.add(new SuggestionItem(true, entry.getSearch()));
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
|
||||||
|
@ -619,19 +588,18 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
return local.materialize();
|
return local.materialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Observable<List<SuggestionItem>> network = ExtractorHelper.suggestionsFor(serviceId, query, contentCountry).toObservable()
|
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||||
.map(new Function<List<String>, List<SuggestionItem>>() {
|
.suggestionsFor(serviceId, query, contentCountry)
|
||||||
@Override
|
.toObservable()
|
||||||
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<String> strings) throws Exception {
|
.map(strings -> {
|
||||||
List<SuggestionItem> result = new ArrayList<>();
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
for (String entry : strings) result.add(new SuggestionItem(false, entry));
|
for (String entry : strings) {
|
||||||
return result;
|
result.add(new SuggestionItem(false, entry));
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Observable.zip(local, network, new BiFunction<List<SuggestionItem>, List<SuggestionItem>, List<SuggestionItem>>() {
|
return Observable.zip(local, network, (localResult, networkResult) -> {
|
||||||
@Override
|
|
||||||
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SuggestionItem> localResult, @io.reactivex.annotations.NonNull List<SuggestionItem> networkResult) throws Exception {
|
|
||||||
List<SuggestionItem> result = new ArrayList<>();
|
List<SuggestionItem> result = new ArrayList<>();
|
||||||
if (localResult.size() > 0) result.addAll(localResult);
|
if (localResult.size() > 0) result.addAll(localResult);
|
||||||
|
|
||||||
|
@ -649,25 +617,21 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
|
|
||||||
if (networkResult.size() > 0) result.addAll(networkResult);
|
if (networkResult.size() > 0) result.addAll(networkResult);
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
}).materialize();
|
}).materialize();
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Notification<List<SuggestionItem>>>() {
|
.subscribe(listNotification -> {
|
||||||
@Override
|
|
||||||
public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> listNotification) throws Exception {
|
|
||||||
if (listNotification.isOnNext()) {
|
if (listNotification.isOnNext()) {
|
||||||
handleSuggestions(listNotification.getValue());
|
handleSuggestions(listNotification.getValue());
|
||||||
} else if (listNotification.isOnError()) {
|
} else if (listNotification.isOnError()) {
|
||||||
Throwable error = listNotification.getError();
|
Throwable error = listNotification.getError();
|
||||||
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
|
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
|
||||||
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) {
|
IOException.class, SocketException.class,
|
||||||
|
InterruptedException.class, InterruptedIOException.class)) {
|
||||||
onSuggestionError(error);
|
onSuggestionError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -718,11 +682,14 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
hideSuggestionsPanel();
|
hideSuggestionsPanel();
|
||||||
hideKeyboardSearch();
|
hideKeyboardSearch();
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
historyRecordManager.onSearched(serviceId, query)
|
||||||
((HistoryListener) activity).onSearch(serviceId, query);
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {},
|
||||||
|
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||||
|
NewPipe.getNameOfService(serviceId), query, 0)
|
||||||
|
);
|
||||||
suggestionPublisher.onNext(query);
|
suggestionPublisher.onNext(query);
|
||||||
}
|
|
||||||
|
|
||||||
startLoading(false);
|
startLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class HeaderFooterHolder extends RecyclerView.ViewHolder {
|
||||||
|
public View view;
|
||||||
|
|
||||||
|
public HeaderFooterHolder(View v) {
|
||||||
|
super(v);
|
||||||
|
view = v;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 26.09.16.
|
||||||
|
* <p>
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* InfoItemBuilder.java is part of NewPipe.
|
||||||
|
* <p>
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
* <p>
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LocalItemBuilder {
|
||||||
|
private static final String TAG = LocalItemBuilder.class.toString();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||||
|
|
||||||
|
private OnClickGesture<LocalItem> onSelectedListener;
|
||||||
|
|
||||||
|
public LocalItemBuilder(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void displayImage(final String url, final ImageView view,
|
||||||
|
final DisplayImageOptions options) {
|
||||||
|
imageLoader.displayImage(url, view, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OnClickGesture<LocalItem> getOnItemSelectedListener() {
|
||||||
|
return onSelectedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnItemSelectedListener(OnClickGesture<LocalItem> listener) {
|
||||||
|
this.onSelectedListener = listener;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.fragments.local.holder.LocalItemHolder;
|
||||||
|
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder;
|
||||||
|
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder;
|
||||||
|
import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder;
|
||||||
|
import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* InfoListAdapter.java is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
private static final String TAG = LocalItemListAdapter.class.getSimpleName();
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private static final int HEADER_TYPE = 0;
|
||||||
|
private static final int FOOTER_TYPE = 1;
|
||||||
|
|
||||||
|
private static final int STREAM_STATISTICS_HOLDER_TYPE = 0x1000;
|
||||||
|
private static final int STREAM_PLAYLIST_HOLDER_TYPE = 0x1001;
|
||||||
|
private static final int LOCAL_PLAYLIST_HOLDER_TYPE = 0x2000;
|
||||||
|
private static final int REMOTE_PLAYLIST_HOLDER_TYPE = 0x2001;
|
||||||
|
|
||||||
|
private final LocalItemBuilder localItemBuilder;
|
||||||
|
private final ArrayList<LocalItem> localItems;
|
||||||
|
private final DateFormat dateFormat;
|
||||||
|
|
||||||
|
private boolean showFooter = false;
|
||||||
|
private View header = null;
|
||||||
|
private View footer = null;
|
||||||
|
|
||||||
|
public LocalItemListAdapter(Activity activity) {
|
||||||
|
localItemBuilder = new LocalItemBuilder(activity);
|
||||||
|
localItems = new ArrayList<>();
|
||||||
|
dateFormat = DateFormat.getDateInstance(DateFormat.SHORT,
|
||||||
|
Localization.getPreferredLocale(activity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedListener(OnClickGesture<LocalItem> listener) {
|
||||||
|
localItemBuilder.setOnItemSelectedListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unsetSelectedListener() {
|
||||||
|
localItemBuilder.setOnItemSelectedListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addItems(List<? extends LocalItem> data) {
|
||||||
|
if (data != null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addItems() before > localItems.size() = " +
|
||||||
|
localItems.size() + ", data.size() = " + data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int offsetStart = sizeConsideringHeader();
|
||||||
|
localItems.addAll(data);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart +
|
||||||
|
", localItems.size() = " + localItems.size() +
|
||||||
|
", header = " + header + ", footer = " + footer +
|
||||||
|
", showFooter = " + showFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyItemRangeInserted(offsetStart, data.size());
|
||||||
|
|
||||||
|
if (footer != null && showFooter) {
|
||||||
|
int footerNow = sizeConsideringHeader();
|
||||||
|
notifyItemMoved(offsetStart, footerNow);
|
||||||
|
|
||||||
|
if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart +
|
||||||
|
" to " + footerNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeItem(final LocalItem data) {
|
||||||
|
final int index = localItems.indexOf(data);
|
||||||
|
|
||||||
|
localItems.remove(index);
|
||||||
|
notifyItemRemoved(index + (header != null ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean swapItems(int fromAdapterPosition, int toAdapterPosition) {
|
||||||
|
final int actualFrom = adapterOffsetWithoutHeader(fromAdapterPosition);
|
||||||
|
final int actualTo = adapterOffsetWithoutHeader(toAdapterPosition);
|
||||||
|
|
||||||
|
if (actualFrom < 0 || actualTo < 0) return false;
|
||||||
|
if (actualFrom >= localItems.size() || actualTo >= localItems.size()) return false;
|
||||||
|
|
||||||
|
localItems.add(actualTo, localItems.remove(actualFrom));
|
||||||
|
notifyItemMoved(fromAdapterPosition, toAdapterPosition);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearStreamItemList() {
|
||||||
|
if (localItems.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localItems.clear();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeader(View header) {
|
||||||
|
boolean changed = header != this.header;
|
||||||
|
this.header = header;
|
||||||
|
if (changed) notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFooter(View view) {
|
||||||
|
this.footer = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showFooter(boolean show) {
|
||||||
|
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||||
|
if (show == showFooter) return;
|
||||||
|
|
||||||
|
showFooter = show;
|
||||||
|
if (show) notifyItemInserted(sizeConsideringHeader());
|
||||||
|
else notifyItemRemoved(sizeConsideringHeader());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int adapterOffsetWithoutHeader(final int offset) {
|
||||||
|
return offset - (header != null ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int sizeConsideringHeader() {
|
||||||
|
return localItems.size() + (header != null ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<LocalItem> getItemsList() {
|
||||||
|
return localItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
int count = localItems.size();
|
||||||
|
if (header != null) count++;
|
||||||
|
if (footer != null && showFooter) count++;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "getItemCount() called, count = " + count +
|
||||||
|
", localItems.size() = " + localItems.size() +
|
||||||
|
", header = " + header + ", footer = " + footer +
|
||||||
|
", showFooter = " + showFooter);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||||
|
|
||||||
|
if (header != null && position == 0) {
|
||||||
|
return HEADER_TYPE;
|
||||||
|
} else if (header != null) {
|
||||||
|
position--;
|
||||||
|
}
|
||||||
|
if (footer != null && position == localItems.size() && showFooter) {
|
||||||
|
return FOOTER_TYPE;
|
||||||
|
}
|
||||||
|
final LocalItem item = localItems.get(position);
|
||||||
|
|
||||||
|
switch (item.getLocalItemType()) {
|
||||||
|
case PLAYLIST_LOCAL_ITEM: return LOCAL_PLAYLIST_HOLDER_TYPE;
|
||||||
|
case PLAYLIST_REMOTE_ITEM: return REMOTE_PLAYLIST_HOLDER_TYPE;
|
||||||
|
|
||||||
|
case PLAYLIST_STREAM_ITEM: return STREAM_PLAYLIST_HOLDER_TYPE;
|
||||||
|
case STATISTIC_STREAM_ITEM: return STREAM_STATISTICS_HOLDER_TYPE;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "No holder type has been considered for item: [" +
|
||||||
|
item.getLocalItemType() + "]");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" +
|
||||||
|
parent + "], type = [" + type + "]");
|
||||||
|
switch (type) {
|
||||||
|
case HEADER_TYPE:
|
||||||
|
return new HeaderFooterHolder(header);
|
||||||
|
case FOOTER_TYPE:
|
||||||
|
return new HeaderFooterHolder(footer);
|
||||||
|
case LOCAL_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new LocalPlaylistItemHolder(localItemBuilder, parent);
|
||||||
|
case REMOTE_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new RemotePlaylistItemHolder(localItemBuilder, parent);
|
||||||
|
case STREAM_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new LocalPlaylistStreamItemHolder(localItemBuilder, parent);
|
||||||
|
case STREAM_STATISTICS_HOLDER_TYPE:
|
||||||
|
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" +
|
||||||
|
holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||||
|
|
||||||
|
if (holder instanceof LocalItemHolder) {
|
||||||
|
// If header isn't null, offset the items by -1
|
||||||
|
if (header != null) position--;
|
||||||
|
|
||||||
|
((LocalItemHolder) holder).updateFromItem(localItems.get(position), dateFormat);
|
||||||
|
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
||||||
|
((HeaderFooterHolder) holder).view = header;
|
||||||
|
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||||
|
&& footer != null && showFooter) {
|
||||||
|
((HeaderFooterHolder) holder).view = footer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Maybe;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class LocalPlaylistManager {
|
||||||
|
|
||||||
|
private final AppDatabase database;
|
||||||
|
private final StreamDAO streamTable;
|
||||||
|
private final PlaylistDAO playlistTable;
|
||||||
|
private final PlaylistStreamDAO playlistStreamTable;
|
||||||
|
|
||||||
|
public LocalPlaylistManager(final AppDatabase db) {
|
||||||
|
database = db;
|
||||||
|
streamTable = db.streamDAO();
|
||||||
|
playlistTable = db.playlistDAO();
|
||||||
|
playlistStreamTable = db.playlistStreamDAO();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<List<Long>> createPlaylist(final String name, final List<StreamEntity> streams) {
|
||||||
|
// Disallow creation of empty playlists
|
||||||
|
if (streams.isEmpty()) return Maybe.empty();
|
||||||
|
final StreamEntity defaultStream = streams.get(0);
|
||||||
|
final PlaylistEntity newPlaylist =
|
||||||
|
new PlaylistEntity(name, defaultStream.getThumbnailUrl());
|
||||||
|
|
||||||
|
return Maybe.fromCallable(() -> database.runInTransaction(() ->
|
||||||
|
upsertStreams(playlistTable.insert(newPlaylist), streams, 0))
|
||||||
|
).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<List<Long>> appendToPlaylist(final long playlistId,
|
||||||
|
final List<StreamEntity> streams) {
|
||||||
|
return playlistStreamTable.getMaximumIndexOf(playlistId)
|
||||||
|
.firstElement()
|
||||||
|
.map(maxJoinIndex -> database.runInTransaction(() ->
|
||||||
|
upsertStreams(playlistId, streams, maxJoinIndex + 1))
|
||||||
|
).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> upsertStreams(final long playlistId,
|
||||||
|
final List<StreamEntity> streams,
|
||||||
|
final int indexOffset) {
|
||||||
|
|
||||||
|
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streams.size());
|
||||||
|
final List<Long> streamIds = streamTable.upsertAll(streams);
|
||||||
|
for (int index = 0; index < streamIds.size(); index++) {
|
||||||
|
joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(index),
|
||||||
|
index + indexOffset));
|
||||||
|
}
|
||||||
|
return playlistStreamTable.insertAll(joinEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Completable updateJoin(final long playlistId, final List<Long> streamIds) {
|
||||||
|
List<PlaylistStreamEntity> joinEntities = new ArrayList<>(streamIds.size());
|
||||||
|
for (int i = 0; i < streamIds.size(); i++) {
|
||||||
|
joinEntities.add(new PlaylistStreamEntity(playlistId, streamIds.get(i), i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Completable.fromRunnable(() -> database.runInTransaction(() -> {
|
||||||
|
playlistStreamTable.deleteBatch(playlistId);
|
||||||
|
playlistStreamTable.insertAll(joinEntities);
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
|
||||||
|
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
|
||||||
|
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||||
|
return Single.fromCallable(() -> playlistTable.deletePlaylist(playlistId))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<Integer> renamePlaylist(final long playlistId, final String name) {
|
||||||
|
return modifyPlaylist(playlistId, name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<Integer> changePlaylistThumbnail(final long playlistId,
|
||||||
|
final String thumbnailUrl) {
|
||||||
|
return modifyPlaylist(playlistId, null, thumbnailUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Maybe<Integer> modifyPlaylist(final long playlistId,
|
||||||
|
@Nullable final String name,
|
||||||
|
@Nullable final String thumbnailUrl) {
|
||||||
|
return playlistTable.getPlaylist(playlistId)
|
||||||
|
.firstElement()
|
||||||
|
.filter(playlistEntities -> !playlistEntities.isEmpty())
|
||||||
|
.map(playlistEntities -> {
|
||||||
|
PlaylistEntity playlist = playlistEntities.get(0);
|
||||||
|
if (name != null) playlist.setName(name);
|
||||||
|
if (thumbnailUrl != null) playlist.setThumbnailUrl(thumbnailUrl);
|
||||||
|
return playlistTable.update(playlist);
|
||||||
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class RemotePlaylistManager {
|
||||||
|
|
||||||
|
private final AppDatabase database;
|
||||||
|
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||||
|
|
||||||
|
public RemotePlaylistManager(final AppDatabase db) {
|
||||||
|
database = db;
|
||||||
|
playlistRemoteTable = db.playlistRemoteDAO();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylists() {
|
||||||
|
return playlistRemoteTable.getAll().subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<PlaylistRemoteEntity>> getPlaylist(final PlaylistInfo info) {
|
||||||
|
return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl())
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deletePlaylist(final long playlistId) {
|
||||||
|
return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Long> onBookmark(final PlaylistInfo playlistInfo) {
|
||||||
|
return Single.fromCallable(() -> {
|
||||||
|
final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||||
|
return playlistRemoteTable.upsert(playlist);
|
||||||
|
}).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) {
|
||||||
|
return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo)))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v7.app.ActionBar;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
import org.schabi.newpipe.fragments.list.ListViewContract;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This fragment is design to be used with persistent data such as
|
||||||
|
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
|
||||||
|
* in the list adapter to avoid extra writes when the it exits or re-enters its lifecycle.
|
||||||
|
*
|
||||||
|
* This fragment destroys its adapter and views when {@link Fragment#onDestroyView()} is
|
||||||
|
* called and is memory efficient when in backstack.
|
||||||
|
* */
|
||||||
|
public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||||
|
implements ListViewContract<I, N> {
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Views
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
protected View headerRootView;
|
||||||
|
protected View footerRootView;
|
||||||
|
|
||||||
|
protected LocalItemListAdapter itemListAdapter;
|
||||||
|
protected RecyclerView itemsList;
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Lifecycle - Creation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Lifecycle - View
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
protected View getListHeader() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected View getListFooter() {
|
||||||
|
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||||
|
return new LinearLayoutManager(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
itemsList = rootView.findViewById(R.id.items_list);
|
||||||
|
itemsList.setLayoutManager(getListLayoutManager());
|
||||||
|
|
||||||
|
itemListAdapter = new LocalItemListAdapter(activity);
|
||||||
|
itemListAdapter.setHeader(headerRootView = getListHeader());
|
||||||
|
itemListAdapter.setFooter(footerRootView = getListFooter());
|
||||||
|
|
||||||
|
itemsList.setAdapter(itemListAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Lifecycle - Menu
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||||
|
"], inflater = [" + inflater + "]");
|
||||||
|
|
||||||
|
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
|
if (supportActionBar == null) return;
|
||||||
|
|
||||||
|
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Lifecycle - Destruction
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
itemsList = null;
|
||||||
|
itemListAdapter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Contract
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
resetFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showLoading() {
|
||||||
|
super.showLoading();
|
||||||
|
if (itemsList != null) animateView(itemsList, false, 200);
|
||||||
|
if (headerRootView != null) animateView(headerRootView, false, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideLoading() {
|
||||||
|
super.hideLoading();
|
||||||
|
if (itemsList != null) animateView(itemsList, true, 200);
|
||||||
|
if (headerRootView != null) animateView(headerRootView, true, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showError(String message, boolean showRetryButton) {
|
||||||
|
super.showError(message, showRetryButton);
|
||||||
|
showListFooter(false);
|
||||||
|
|
||||||
|
if (itemsList != null) animateView(itemsList, false, 200);
|
||||||
|
if (headerRootView != null) animateView(headerRootView, false, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showEmptyState() {
|
||||||
|
super.showEmptyState();
|
||||||
|
showListFooter(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showListFooter(final boolean show) {
|
||||||
|
itemsList.post(() -> itemListAdapter.showFooter(show));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNextItems(N result) {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Error handling
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
protected void resetFragment() {
|
||||||
|
if (itemListAdapter != null) itemListAdapter.clearStreamItemList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
resetFragment();
|
||||||
|
return super.onError(exception);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,309 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscriber;
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
|
||||||
|
public final class BookmarkFragment
|
||||||
|
extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||||
|
|
||||||
|
private View lastPlayedButton;
|
||||||
|
private View mostPlayedButton;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected Parcelable itemsListState;
|
||||||
|
|
||||||
|
private Subscription databaseSubscription;
|
||||||
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Creation
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
|
||||||
|
localPlaylistManager = new LocalPlaylistManager(database);
|
||||||
|
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||||
|
disposables = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
if (activity != null && activity.getSupportActionBar() != null) {
|
||||||
|
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||||
|
activity.setTitle(R.string.tab_subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
|
if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Views
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View getListHeader() {
|
||||||
|
final View headerRootLayout = activity.getLayoutInflater()
|
||||||
|
.inflate(R.layout.bookmark_header, itemsList, false);
|
||||||
|
lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed);
|
||||||
|
mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed);
|
||||||
|
return headerRootLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
|
||||||
|
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||||
|
@Override
|
||||||
|
public void selected(LocalItem selectedItem) {
|
||||||
|
// Requires the parent fragment to find holder for fragment replacement
|
||||||
|
if (getParentFragment() == null) return;
|
||||||
|
final FragmentManager fragmentManager = getParentFragment().getFragmentManager();
|
||||||
|
|
||||||
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
|
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||||
|
NavigationHelper.openLocalPlaylistFragment(fragmentManager, entry.uid,
|
||||||
|
entry.name);
|
||||||
|
|
||||||
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
|
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||||
|
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
|
||||||
|
entry.getUrl(), entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void held(LocalItem selectedItem) {
|
||||||
|
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||||
|
showLocalDeleteDialog((PlaylistMetadataEntry) selectedItem);
|
||||||
|
|
||||||
|
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||||
|
showRemoteDeleteDialog((PlaylistRemoteEntity) selectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lastPlayedButton.setOnClickListener(view -> {
|
||||||
|
if (getParentFragment() != null) {
|
||||||
|
NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mostPlayedButton.setOnClickListener(view -> {
|
||||||
|
if (getParentFragment() != null) {
|
||||||
|
NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Loading
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
|
Flowable.combineLatest(
|
||||||
|
localPlaylistManager.getPlaylists(),
|
||||||
|
remotePlaylistManager.getPlaylists(),
|
||||||
|
BookmarkFragment::merge
|
||||||
|
).onBackpressureLatest()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getPlaylistsSubscriber());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Destruction
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null);
|
||||||
|
if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null);
|
||||||
|
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
|
||||||
|
databaseSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (disposables != null) disposables.dispose();
|
||||||
|
|
||||||
|
disposables = null;
|
||||||
|
localPlaylistManager = null;
|
||||||
|
remotePlaylistManager = null;
|
||||||
|
itemsListState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Subscriptions Loader
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private Subscriber<List<PlaylistLocalItem>> getPlaylistsSubscriber() {
|
||||||
|
return new Subscriber<List<PlaylistLocalItem>>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription s) {
|
||||||
|
showLoading();
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
databaseSubscription = s;
|
||||||
|
databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(List<PlaylistLocalItem> subscriptions) {
|
||||||
|
handleResult(subscriptions);
|
||||||
|
if (databaseSubscription != null) databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable exception) {
|
||||||
|
BookmarkFragment.this.onError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull List<PlaylistLocalItem> result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
|
||||||
|
itemListAdapter.clearStreamItemList();
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
showEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemListAdapter.addItems(result);
|
||||||
|
if (itemsListState != null) {
|
||||||
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
|
itemsListState = null;
|
||||||
|
}
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Error Handling
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
if (super.onError(exception)) return true;
|
||||||
|
|
||||||
|
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||||
|
"none", "Bookmark", R.string.general_error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void resetFragment() {
|
||||||
|
super.resetFragment();
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void showLocalDeleteDialog(final PlaylistMetadataEntry item) {
|
||||||
|
showDeleteDialog(item.name, localPlaylistManager.deletePlaylist(item.uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
|
||||||
|
showDeleteDialog(item.getName(), remotePlaylistManager.deletePlaylist(item.getUid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDeleteDialog(final String name, final Single<Integer> deleteReactor) {
|
||||||
|
if (activity == null || disposables == null) return;
|
||||||
|
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(name)
|
||||||
|
.setMessage(R.string.delete_playlist_prompt)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.delete, (dialog, i) ->
|
||||||
|
disposables.add(deleteReactor
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> {/*Do nothing on success*/}, this::onError))
|
||||||
|
)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PlaylistLocalItem> merge(final List<PlaylistMetadataEntry> localPlaylists,
|
||||||
|
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||||
|
List<PlaylistLocalItem> items = new ArrayList<>(
|
||||||
|
localPlaylists.size() + remotePlaylists.size());
|
||||||
|
items.addAll(localPlaylists);
|
||||||
|
items.addAll(remotePlaylists);
|
||||||
|
|
||||||
|
Collections.sort(items, (left, right) ->
|
||||||
|
left.getOrderingName().compareToIgnoreCase(right.getOrderingName()));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class LastPlayedFragment extends StatisticsPlaylistFragment {
|
||||||
|
@Override
|
||||||
|
protected String getName() {
|
||||||
|
return getString(R.string.title_last_played);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||||
|
Collections.sort(results, (left, right) ->
|
||||||
|
right.latestAccessDate.compareTo(left.latestAccessDate));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,590 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscriber;
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
|
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.disposables.Disposables;
|
||||||
|
import io.reactivex.subjects.PublishSubject;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
|
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
|
||||||
|
|
||||||
|
// Save the list 10 seconds after the last change occurred
|
||||||
|
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
|
||||||
|
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
|
||||||
|
|
||||||
|
private View headerRootLayout;
|
||||||
|
private TextView headerTitleView;
|
||||||
|
private TextView headerStreamCount;
|
||||||
|
|
||||||
|
private View playlistControl;
|
||||||
|
private View headerPlayAllButton;
|
||||||
|
private View headerPopupButton;
|
||||||
|
private View headerBackgroundButton;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected Long playlistId;
|
||||||
|
@State
|
||||||
|
protected String name;
|
||||||
|
@State
|
||||||
|
protected Parcelable itemsListState;
|
||||||
|
|
||||||
|
private ItemTouchHelper itemTouchHelper;
|
||||||
|
|
||||||
|
private LocalPlaylistManager playlistManager;
|
||||||
|
private Subscription databaseSubscription;
|
||||||
|
|
||||||
|
private PublishSubject<Long> debouncedSaveSignal;
|
||||||
|
private CompositeDisposable disposables;
|
||||||
|
|
||||||
|
/* Has the playlist been fully loaded from db */
|
||||||
|
private AtomicBoolean isLoadingComplete;
|
||||||
|
/* Has the playlist been modified (e.g. items reordered or deleted) */
|
||||||
|
private AtomicBoolean isModified;
|
||||||
|
|
||||||
|
public static LocalPlaylistFragment getInstance(long playlistId, String name) {
|
||||||
|
LocalPlaylistFragment instance = new LocalPlaylistFragment();
|
||||||
|
instance.setInitialData(playlistId, name);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Creation
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
playlistManager = new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||||
|
debouncedSaveSignal = PublishSubject.create();
|
||||||
|
|
||||||
|
disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
isLoadingComplete = new AtomicBoolean();
|
||||||
|
isModified = new AtomicBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Lifecycle - Views
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(final String title) {
|
||||||
|
super.setTitle(title);
|
||||||
|
|
||||||
|
if (headerTitleView != null) {
|
||||||
|
headerTitleView.setText(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
setTitle(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View getListHeader() {
|
||||||
|
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.local_playlist_header,
|
||||||
|
itemsList, false);
|
||||||
|
|
||||||
|
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||||
|
headerTitleView.setSelected(true);
|
||||||
|
|
||||||
|
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
|
||||||
|
|
||||||
|
playlistControl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||||
|
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||||
|
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||||
|
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||||
|
|
||||||
|
return headerRootLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
|
||||||
|
headerTitleView.setOnClickListener(view -> createRenameDialog());
|
||||||
|
|
||||||
|
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
|
||||||
|
itemTouchHelper.attachToRecyclerView(itemsList);
|
||||||
|
|
||||||
|
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||||
|
@Override
|
||||||
|
public void selected(LocalItem selectedItem) {
|
||||||
|
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||||
|
final PlaylistStreamEntry item = (PlaylistStreamEntry) selectedItem;
|
||||||
|
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
|
||||||
|
item.serviceId, item.url, item.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void held(LocalItem selectedItem) {
|
||||||
|
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||||
|
showStreamDialog((PlaylistStreamEntry) selectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drag(LocalItem selectedItem, RecyclerView.ViewHolder viewHolder) {
|
||||||
|
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Lifecycle - Loading
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showLoading() {
|
||||||
|
super.showLoading();
|
||||||
|
if (headerRootLayout != null) animateView(headerRootLayout, false, 200);
|
||||||
|
if (playlistControl != null) animateView(playlistControl, false, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hideLoading() {
|
||||||
|
super.hideLoading();
|
||||||
|
if (headerRootLayout != null) animateView(headerRootLayout, true, 200);
|
||||||
|
if (playlistControl != null) animateView(playlistControl, true, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
disposables.add(getDebouncedSaver());
|
||||||
|
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
isModified.set(false);
|
||||||
|
|
||||||
|
playlistManager.getPlaylistStreams(playlistId)
|
||||||
|
.onBackpressureLatest()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getPlaylistObserver());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Lifecycle - Destruction
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
|
||||||
|
// Save on exit
|
||||||
|
saveImmediate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
|
||||||
|
if (itemListAdapter != null) itemListAdapter.unsetSelectedListener();
|
||||||
|
if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null);
|
||||||
|
if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null);
|
||||||
|
if (headerPopupButton != null) headerPopupButton.setOnClickListener(null);
|
||||||
|
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
if (disposables != null) disposables.clear();
|
||||||
|
|
||||||
|
databaseSubscription = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (debouncedSaveSignal != null) debouncedSaveSignal.onComplete();
|
||||||
|
if (disposables != null) disposables.dispose();
|
||||||
|
|
||||||
|
debouncedSaveSignal = null;
|
||||||
|
playlistManager = null;
|
||||||
|
disposables = null;
|
||||||
|
|
||||||
|
isLoadingComplete = null;
|
||||||
|
isModified = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist Stream Loader
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private Subscriber<List<PlaylistStreamEntry>> getPlaylistObserver() {
|
||||||
|
return new Subscriber<List<PlaylistStreamEntry>>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription s) {
|
||||||
|
showLoading();
|
||||||
|
isLoadingComplete.set(false);
|
||||||
|
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
databaseSubscription = s;
|
||||||
|
databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(List<PlaylistStreamEntry> streams) {
|
||||||
|
// Skip handling the result after it has been modified
|
||||||
|
if (isModified == null || !isModified.get()) {
|
||||||
|
handleResult(streams);
|
||||||
|
isLoadingComplete.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSubscription != null) databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable exception) {
|
||||||
|
LocalPlaylistFragment.this.onError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull List<PlaylistStreamEntry> result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
if (itemListAdapter == null) return;
|
||||||
|
|
||||||
|
itemListAdapter.clearStreamItemList();
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
showEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemListAdapter.addItems(result);
|
||||||
|
if (itemsListState != null) {
|
||||||
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
|
itemsListState = null;
|
||||||
|
}
|
||||||
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
|
||||||
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||||
|
headerPopupButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||||
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Error Handling
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void resetFragment() {
|
||||||
|
super.resetFragment();
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
if (super.onError(exception)) return true;
|
||||||
|
|
||||||
|
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||||
|
"none", "Local Playlist", R.string.general_error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playlist Metadata/Streams Manipulation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void createRenameDialog() {
|
||||||
|
if (playlistId == null || name == null || getContext() == null) return;
|
||||||
|
|
||||||
|
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
|
||||||
|
EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
|
||||||
|
nameEdit.setText(name);
|
||||||
|
nameEdit.setSelection(nameEdit.getText().length());
|
||||||
|
|
||||||
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
|
||||||
|
.setTitle(R.string.rename_playlist)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.rename, (dialogInterface, i) -> {
|
||||||
|
changePlaylistName(nameEdit.getText().toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogBuilder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changePlaylistName(final String name) {
|
||||||
|
if (playlistManager == null) return;
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
setTitle(name);
|
||||||
|
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||||
|
"] with new name=[" + name + "] items");
|
||||||
|
|
||||||
|
final Disposable disposable = playlistManager.renamePlaylist(playlistId, name)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> {/*Do nothing on success*/}, this::onError);
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeThumbnailUrl(final String thumbnailUrl) {
|
||||||
|
if (playlistManager == null) return;
|
||||||
|
|
||||||
|
final Toast successToast = Toast.makeText(getActivity(),
|
||||||
|
R.string.playlist_thumbnail_change_success,
|
||||||
|
Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||||
|
"] with new thumbnail url=[" + thumbnailUrl + "]");
|
||||||
|
|
||||||
|
final Disposable disposable = playlistManager
|
||||||
|
.changePlaylistThumbnail(playlistId, thumbnailUrl)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignore -> successToast.show(), this::onError);
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteItem(final PlaylistStreamEntry item) {
|
||||||
|
if (itemListAdapter == null) return;
|
||||||
|
|
||||||
|
itemListAdapter.removeItem(item);
|
||||||
|
setVideoCount(itemListAdapter.getItemsList().size());
|
||||||
|
saveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveChanges() {
|
||||||
|
if (isModified == null || debouncedSaveSignal == null) return;
|
||||||
|
|
||||||
|
isModified.set(true);
|
||||||
|
debouncedSaveSignal.onNext(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Disposable getDebouncedSaver() {
|
||||||
|
if (debouncedSaveSignal == null) return Disposables.empty();
|
||||||
|
|
||||||
|
return debouncedSaveSignal
|
||||||
|
.debounce(SAVE_DEBOUNCE_MILLIS, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> saveImmediate(), this::onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveImmediate() {
|
||||||
|
if (playlistManager == null || itemListAdapter == null) return;
|
||||||
|
|
||||||
|
// List must be loaded and modified in order to save
|
||||||
|
if (isLoadingComplete == null || isModified == null ||
|
||||||
|
!isLoadingComplete.get() || !isModified.get()) {
|
||||||
|
Log.w(TAG, "Attempting to save playlist when local playlist " +
|
||||||
|
"is not loaded or not modified: playlist id=[" + playlistId + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> items = itemListAdapter.getItemsList();
|
||||||
|
List<Long> streamIds = new ArrayList<>(items.size());
|
||||||
|
for (final LocalItem item : items) {
|
||||||
|
if (item instanceof PlaylistStreamEntry) {
|
||||||
|
streamIds.add(((PlaylistStreamEntry) item).streamId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Updating playlist id=[" + playlistId +
|
||||||
|
"] with [" + streamIds.size() + "] items");
|
||||||
|
|
||||||
|
final Disposable disposable = playlistManager.updateJoin(playlistId, streamIds)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
() -> { if (isModified != null) isModified.set(false); },
|
||||||
|
this::onError
|
||||||
|
);
|
||||||
|
disposables.add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
|
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
|
||||||
|
ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||||
|
@Override
|
||||||
|
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||||
|
int viewSizeOutOfBounds, int totalSize,
|
||||||
|
long msSinceStartScroll) {
|
||||||
|
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||||
|
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||||
|
final int minimumAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
|
Math.abs(standardSpeed));
|
||||||
|
return minimumAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||||
|
RecyclerView.ViewHolder target) {
|
||||||
|
if (source.getItemViewType() != target.getItemViewType() ||
|
||||||
|
itemListAdapter == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sourceIndex = source.getAdapterPosition();
|
||||||
|
final int targetIndex = target.getAdapterPosition();
|
||||||
|
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
|
||||||
|
if (isSwapped) saveChanges();
|
||||||
|
return isSwapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLongPressDragEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isItemViewSwipeEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
protected void showStreamDialog(final PlaylistStreamEntry item) {
|
||||||
|
final Context context = getContext();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||||
|
|
||||||
|
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||||
|
|
||||||
|
final String[] commands = new String[]{
|
||||||
|
context.getResources().getString(R.string.enqueue_on_background),
|
||||||
|
context.getResources().getString(R.string.enqueue_on_popup),
|
||||||
|
context.getResources().getString(R.string.start_here_on_main),
|
||||||
|
context.getResources().getString(R.string.start_here_on_background),
|
||||||
|
context.getResources().getString(R.string.start_here_on_popup),
|
||||||
|
context.getResources().getString(R.string.set_as_playlist_thumbnail),
|
||||||
|
context.getResources().getString(R.string.delete)
|
||||||
|
};
|
||||||
|
|
||||||
|
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||||
|
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
NavigationHelper.enqueueOnBackgroundPlayer(context,
|
||||||
|
new SinglePlayQueue(infoItem));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
NavigationHelper.enqueueOnPopupPlayer(activity, new
|
||||||
|
SinglePlayQueue(infoItem));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
changeThumbnailUrl(item.thumbnailUrl);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
deleteItem(item);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInitialData(long playlistId, String name) {
|
||||||
|
this.playlistId = playlistId;
|
||||||
|
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVideoCount(final long count) {
|
||||||
|
if (activity != null && headerStreamCount != null) {
|
||||||
|
headerStreamCount.setText(Localization.localizeStreamCount(activity, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayQueue getPlayQueue() {
|
||||||
|
return getPlayQueue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return new SinglePlayQueue(Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
|
||||||
|
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
|
||||||
|
for (final LocalItem item : infoItems) {
|
||||||
|
if (item instanceof PlaylistStreamEntry) {
|
||||||
|
streamInfoItems.add(((PlaylistStreamEntry) item).toStreamInfoItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new SinglePlayQueue(streamInfoItems, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class MostPlayedFragment extends StatisticsPlaylistFragment {
|
||||||
|
@Override
|
||||||
|
protected String getName() {
|
||||||
|
return getString(R.string.title_most_played);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||||
|
Collections.sort(results, (left, right) ->
|
||||||
|
((Long) right.watchCount).compareTo(left.watchCount));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,300 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.bookmark;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscriber;
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
|
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
public abstract class StatisticsPlaylistFragment
|
||||||
|
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||||
|
|
||||||
|
private View headerPlayAllButton;
|
||||||
|
private View headerPopupButton;
|
||||||
|
private View headerBackgroundButton;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected Parcelable itemsListState;
|
||||||
|
|
||||||
|
/* Used for independent events */
|
||||||
|
private Subscription databaseSubscription;
|
||||||
|
private HistoryRecordManager recordManager;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Abstracts
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected abstract String getName();
|
||||||
|
|
||||||
|
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Creation
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
recordManager = new HistoryRecordManager(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Views
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
setTitle(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View getListHeader() {
|
||||||
|
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
|
||||||
|
itemsList, false);
|
||||||
|
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||||
|
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||||
|
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||||
|
return headerRootLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
|
||||||
|
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||||
|
@Override
|
||||||
|
public void selected(LocalItem selectedItem) {
|
||||||
|
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||||
|
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
|
||||||
|
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
|
||||||
|
item.serviceId, item.url, item.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void held(LocalItem selectedItem) {
|
||||||
|
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||||
|
showStreamDialog((StreamStatisticsEntry) selectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Loading
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startLoading(boolean forceLoad) {
|
||||||
|
super.startLoading(forceLoad);
|
||||||
|
recordManager.getStreamStatistics()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(getHistoryObserver());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle - Destruction
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
|
||||||
|
if (itemListAdapter != null) itemListAdapter.unsetSelectedListener();
|
||||||
|
if (headerBackgroundButton != null) headerBackgroundButton.setOnClickListener(null);
|
||||||
|
if (headerPlayAllButton != null) headerPlayAllButton.setOnClickListener(null);
|
||||||
|
if (headerPopupButton != null) headerPopupButton.setOnClickListener(null);
|
||||||
|
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
databaseSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
recordManager = null;
|
||||||
|
itemsListState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Statistics Loader
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private Subscriber<List<StreamStatisticsEntry>> getHistoryObserver() {
|
||||||
|
return new Subscriber<List<StreamStatisticsEntry>>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Subscription s) {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
databaseSubscription = s;
|
||||||
|
databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNext(List<StreamStatisticsEntry> streams) {
|
||||||
|
handleResult(streams);
|
||||||
|
if (databaseSubscription != null) databaseSubscription.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable exception) {
|
||||||
|
StatisticsPlaylistFragment.this.onError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResult(@NonNull List<StreamStatisticsEntry> result) {
|
||||||
|
super.handleResult(result);
|
||||||
|
if (itemListAdapter == null) return;
|
||||||
|
|
||||||
|
itemListAdapter.clearStreamItemList();
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
showEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemListAdapter.addItems(processResult(result));
|
||||||
|
if (itemsListState != null) {
|
||||||
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
|
itemsListState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerPlayAllButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||||
|
headerPopupButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||||
|
headerBackgroundButton.setOnClickListener(view ->
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Error Handling
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void resetFragment() {
|
||||||
|
super.resetFragment();
|
||||||
|
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onError(Throwable exception) {
|
||||||
|
if (super.onError(exception)) return true;
|
||||||
|
|
||||||
|
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE,
|
||||||
|
"none", "History Statistics", R.string.general_error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private void showStreamDialog(final StreamStatisticsEntry item) {
|
||||||
|
final Context context = getContext();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||||
|
final StreamInfoItem infoItem = item.toStreamInfoItem();
|
||||||
|
|
||||||
|
final String[] commands = new String[]{
|
||||||
|
context.getResources().getString(R.string.enqueue_on_background),
|
||||||
|
context.getResources().getString(R.string.enqueue_on_popup),
|
||||||
|
context.getResources().getString(R.string.start_here_on_main),
|
||||||
|
context.getResources().getString(R.string.start_here_on_background),
|
||||||
|
context.getResources().getString(R.string.start_here_on_popup),
|
||||||
|
};
|
||||||
|
|
||||||
|
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||||
|
final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0);
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayQueue getPlayQueue() {
|
||||||
|
return getPlayQueue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
|
if (itemListAdapter == null) {
|
||||||
|
return new SinglePlayQueue(Collections.emptyList(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LocalItem> infoItems = itemListAdapter.getItemsList();
|
||||||
|
List<StreamInfoItem> streamInfoItems = new ArrayList<>(infoItems.size());
|
||||||
|
for (final LocalItem item : infoItems) {
|
||||||
|
if (item instanceof StreamStatisticsEntry) {
|
||||||
|
streamInfoItems.add(((StreamStatisticsEntry) item).toStreamInfoItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new SinglePlayQueue(streamInfoItems, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.dialog;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
|
||||||
|
public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||||
|
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
|
||||||
|
|
||||||
|
private RecyclerView playlistRecyclerView;
|
||||||
|
private LocalItemListAdapter playlistAdapter;
|
||||||
|
|
||||||
|
private Disposable playlistReactor;
|
||||||
|
|
||||||
|
public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
|
||||||
|
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||||
|
dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaylistAppendDialog fromStreamInfoItems(final List<StreamInfoItem> items) {
|
||||||
|
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||||
|
List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||||
|
for (final StreamInfoItem item : items) {
|
||||||
|
entities.add(new StreamEntity(item));
|
||||||
|
}
|
||||||
|
dialog.setInfo(entities);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlaylistAppendDialog fromPlayQueueItems(final List<PlayQueueItem> items) {
|
||||||
|
PlaylistAppendDialog dialog = new PlaylistAppendDialog();
|
||||||
|
List<StreamEntity> entities = new ArrayList<>(items.size());
|
||||||
|
for (final PlayQueueItem item : items) {
|
||||||
|
entities.add(new StreamEntity(item));
|
||||||
|
}
|
||||||
|
dialog.setInfo(entities);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle - Creation
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.dialog_playlists, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
final LocalPlaylistManager playlistManager =
|
||||||
|
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||||
|
|
||||||
|
playlistAdapter = new LocalItemListAdapter(getActivity());
|
||||||
|
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||||
|
@Override
|
||||||
|
public void selected(LocalItem selectedItem) {
|
||||||
|
if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null)
|
||||||
|
return;
|
||||||
|
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
|
||||||
|
getStreams());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playlistRecyclerView = view.findViewById(R.id.playlist_list);
|
||||||
|
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
playlistRecyclerView.setAdapter(playlistAdapter);
|
||||||
|
|
||||||
|
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
|
||||||
|
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
|
||||||
|
|
||||||
|
playlistReactor = playlistManager.getPlaylists()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::onPlaylistsReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle - Destruction
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (playlistReactor != null) playlistReactor.dispose();
|
||||||
|
if (playlistAdapter != null) playlistAdapter.unsetSelectedListener();
|
||||||
|
|
||||||
|
playlistReactor = null;
|
||||||
|
playlistRecyclerView = null;
|
||||||
|
playlistAdapter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Helper
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
public void openCreatePlaylistDialog() {
|
||||||
|
if (getStreams() == null || getFragmentManager() == null) return;
|
||||||
|
|
||||||
|
PlaylistCreationDialog.newInstance(getStreams()).show(getFragmentManager(), TAG);
|
||||||
|
getDialog().dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
|
||||||
|
if (playlists.isEmpty()) {
|
||||||
|
openCreatePlaylistDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlistAdapter != null && playlistRecyclerView != null) {
|
||||||
|
playlistAdapter.clearStreamItemList();
|
||||||
|
playlistAdapter.addItems(playlists);
|
||||||
|
playlistRecyclerView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
|
||||||
|
@NonNull PlaylistMetadataEntry playlist,
|
||||||
|
@Nonnull List<StreamEntity> streams) {
|
||||||
|
if (getStreams() == null) return;
|
||||||
|
|
||||||
|
@SuppressLint("ShowToast")
|
||||||
|
final Toast successToast = Toast.makeText(getContext(),
|
||||||
|
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
|
manager.appendToPlaylist(playlist.uid, streams)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> successToast.show());
|
||||||
|
|
||||||
|
getDialog().dismiss();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.dialog;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
public final class PlaylistCreationDialog extends PlaylistDialog {
|
||||||
|
private static final String TAG = PlaylistCreationDialog.class.getCanonicalName();
|
||||||
|
|
||||||
|
public static PlaylistCreationDialog newInstance(final List<StreamEntity> streams) {
|
||||||
|
PlaylistCreationDialog dialog = new PlaylistCreationDialog();
|
||||||
|
dialog.setInfo(streams);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Dialog
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
|
if (getStreams() == null) return super.onCreateDialog(savedInstanceState);
|
||||||
|
|
||||||
|
View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
|
||||||
|
EditText nameInput = dialogView.findViewById(R.id.playlist_name);
|
||||||
|
|
||||||
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
|
||||||
|
.setTitle(R.string.create_playlist)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
|
||||||
|
final String name = nameInput.getText().toString();
|
||||||
|
final LocalPlaylistManager playlistManager =
|
||||||
|
new LocalPlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||||
|
final Toast successToast = Toast.makeText(getActivity(),
|
||||||
|
R.string.playlist_creation_success,
|
||||||
|
Toast.LENGTH_SHORT);
|
||||||
|
|
||||||
|
playlistManager.createPlaylist(name, getStreams())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(longs -> successToast.show());
|
||||||
|
});
|
||||||
|
|
||||||
|
return dialogBuilder.create();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.dialog;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
|
||||||
|
|
||||||
|
private List<StreamEntity> streamEntities;
|
||||||
|
|
||||||
|
private StateSaver.SavedState savedState;
|
||||||
|
|
||||||
|
protected void setInfo(final List<StreamEntity> entities) {
|
||||||
|
this.streamEntities = entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<StreamEntity> getStreams() {
|
||||||
|
return streamEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// LifeCycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
savedState = StateSaver.tryToRestore(savedInstanceState, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
StateSaver.onDestroy(savedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// State Saving
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSuffix() {
|
||||||
|
final int size = streamEntities == null ? 0 : streamEntities.size();
|
||||||
|
return "." + size + ".list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(Queue<Object> objectsToSave) {
|
||||||
|
objectsToSave.add(streamEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||||
|
streamEntities = (List<StreamEntity>) savedObjects.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (getActivity() != null) {
|
||||||
|
savedState = StateSaver.tryToSave(getActivity().isChangingConfigurations(),
|
||||||
|
savedState, outState, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.support.annotation.DimenRes;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 12.02.17.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* InfoItemHolder.java is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
|
||||||
|
protected final LocalItemBuilder itemBuilder;
|
||||||
|
|
||||||
|
public LocalItemHolder(LocalItemBuilder itemBuilder, int layoutId, ViewGroup parent) {
|
||||||
|
super(LayoutInflater.from(itemBuilder.getContext())
|
||||||
|
.inflate(layoutId, parent, false));
|
||||||
|
this.itemBuilder = itemBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// ImageLoaderOptions
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base display options
|
||||||
|
*/
|
||||||
|
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cacheInMemory(true)
|
||||||
|
.cacheOnDisk(true)
|
||||||
|
.bitmapConfig(Bitmap.Config.RGB_565)
|
||||||
|
.resetViewBeforeLoading(false)
|
||||||
|
.build();
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||||
|
|
||||||
|
public LocalPlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||||
|
if (!(localItem instanceof PlaylistMetadataEntry)) return;
|
||||||
|
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
||||||
|
|
||||||
|
itemTitleView.setText(item.name);
|
||||||
|
itemStreamCountView.setText(String.valueOf(item.streamCount));
|
||||||
|
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, dateFormat);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||||
|
|
||||||
|
public final ImageView itemThumbnailView;
|
||||||
|
public final TextView itemVideoTitleView;
|
||||||
|
public final TextView itemAdditionalDetailsView;
|
||||||
|
public final TextView itemDurationView;
|
||||||
|
public final View itemHandleView;
|
||||||
|
|
||||||
|
LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||||
|
itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||||
|
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||||
|
itemHandleView = itemView.findViewById(R.id.itemHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_stream_playlist_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||||
|
if (!(localItem instanceof PlaylistStreamEntry)) return;
|
||||||
|
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
|
||||||
|
|
||||||
|
itemVideoTitleView.setText(item.title);
|
||||||
|
itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.uploader,
|
||||||
|
NewPipe.getNameOfService(item.serviceId)));
|
||||||
|
|
||||||
|
if (item.duration > 0) {
|
||||||
|
itemDurationView.setText(Localization.getDurationString(item.duration));
|
||||||
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
|
R.color.duration_background_color));
|
||||||
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
itemDurationView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
|
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setLongClickable(true);
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().held(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
itemThumbnailView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
itemHandleView.setOnTouchListener(getOnTouchListener(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) {
|
||||||
|
return (view, motionEvent) -> {
|
||||||
|
view.performClick();
|
||||||
|
if (itemBuilder != null && itemBuilder.getOnItemSelectedListener() != null &&
|
||||||
|
motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().drag(item,
|
||||||
|
LocalPlaylistStreamItemHolder.this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display options for stream thumbnails
|
||||||
|
*/
|
||||||
|
private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||||
|
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||||
|
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||||
|
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||||
|
.build();
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
|
* <p>
|
||||||
|
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||||
|
* StreamInfoItemHolder.java is part of NewPipe.
|
||||||
|
* <p>
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
* <p>
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
* <p>
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||||
|
|
||||||
|
public final ImageView itemThumbnailView;
|
||||||
|
public final TextView itemVideoTitleView;
|
||||||
|
public final TextView itemUploaderView;
|
||||||
|
public final TextView itemDurationView;
|
||||||
|
public final TextView itemAdditionalDetails;
|
||||||
|
|
||||||
|
public LocalStatisticStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, R.layout.list_stream_item, parent);
|
||||||
|
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
|
||||||
|
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||||
|
itemDurationView = itemView.findViewById(R.id.itemDurationView);
|
||||||
|
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStreamInfoDetailLine(final StreamStatisticsEntry entry,
|
||||||
|
final DateFormat dateFormat) {
|
||||||
|
final String watchCount = Localization.shortViewCount(itemBuilder.getContext(),
|
||||||
|
entry.watchCount);
|
||||||
|
final String uploadDate = dateFormat.format(entry.latestAccessDate);
|
||||||
|
final String serviceName = NewPipe.getNameOfService(entry.serviceId);
|
||||||
|
return Localization.concatenateStrings(watchCount, uploadDate, serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||||
|
if (!(localItem instanceof StreamStatisticsEntry)) return;
|
||||||
|
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
|
||||||
|
|
||||||
|
itemVideoTitleView.setText(item.title);
|
||||||
|
itemUploaderView.setText(item.uploader);
|
||||||
|
|
||||||
|
if (item.duration > 0) {
|
||||||
|
itemDurationView.setText(Localization.getDurationString(item.duration));
|
||||||
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
|
R.color.duration_background_color));
|
||||||
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
itemDurationView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
|
||||||
|
|
||||||
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
|
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setLongClickable(true);
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().held(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display options for stream thumbnails
|
||||||
|
*/
|
||||||
|
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||||
|
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||||
|
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||||
|
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||||
|
.build();
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
public abstract class PlaylistItemHolder extends LocalItemHolder {
|
||||||
|
public final ImageView itemThumbnailView;
|
||||||
|
public final TextView itemStreamCountView;
|
||||||
|
public final TextView itemTitleView;
|
||||||
|
public final TextView itemUploaderView;
|
||||||
|
|
||||||
|
public PlaylistItemHolder(LocalItemBuilder infoItemBuilder,
|
||||||
|
int layoutId, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
|
||||||
|
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().selected(localItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setLongClickable(true);
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnItemSelectedListener().held(localItem);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display options for playlist thumbnails
|
||||||
|
*/
|
||||||
|
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||||
|
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.build();
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||||
|
public RemotePlaylistItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) {
|
||||||
|
if (!(localItem instanceof PlaylistRemoteEntity)) return;
|
||||||
|
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
||||||
|
|
||||||
|
itemTitleView.setText(item.getName());
|
||||||
|
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
|
||||||
|
itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(),
|
||||||
|
NewPipe.getNameOfService(item.getServiceId())));
|
||||||
|
|
||||||
|
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||||
|
DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, dateFormat);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,10 +16,10 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|
||||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -125,24 +125,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
|
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||||
@Override
|
@Override
|
||||||
public void selected(ChannelInfoItem selectedItem) {
|
public void selected(ChannelInfoItem selectedItem) {
|
||||||
// Requires the parent fragment to find holder for fragment replacement
|
// Requires the parent fragment to find holder for fragment replacement
|
||||||
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
|
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(),
|
||||||
|
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void held(ChannelInfoItem selectedItem) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
headerRootLayout.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
headerRootLayout.setOnClickListener(view ->
|
||||||
|
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetFragment() {
|
private void resetFragment() {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.support.v4.app.FragmentStatePagerAdapter;
|
||||||
import android.support.v4.view.ViewPager;
|
import android.support.v4.view.ViewPager;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
@ -22,7 +21,6 @@ import org.schabi.newpipe.settings.SettingsActivity;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
|
|
||||||
public class HistoryActivity extends AppCompatActivity {
|
public class HistoryActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@ -50,8 +48,10 @@ public class HistoryActivity extends AppCompatActivity {
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
getSupportActionBar().setTitle(R.string.title_activity_history);
|
getSupportActionBar().setTitle(R.string.title_activity_history);
|
||||||
|
}
|
||||||
// Create the adapter that will return a fragment for each of the three
|
// Create the adapter that will return a fragment for each of the three
|
||||||
// primary sections of the activity.
|
// primary sections of the activity.
|
||||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||||
|
@ -66,17 +66,11 @@ public class HistoryActivity extends AppCompatActivity {
|
||||||
final FloatingActionButton fab = findViewById(R.id.fab);
|
final FloatingActionButton fab = findViewById(R.id.fab);
|
||||||
RxView.clicks(fab)
|
RxView.clicks(fab)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Object>() {
|
.subscribe(ignored -> {
|
||||||
@Override
|
|
||||||
public void accept(Object o) {
|
|
||||||
int currentItem = mViewPager.getCurrentItem();
|
int currentItem = mViewPager.getCurrentItem();
|
||||||
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem);
|
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter
|
||||||
if(fragment != null) {
|
.instantiateItem(mViewPager, currentItem);
|
||||||
fragment.onHistoryCleared();
|
fragment.onHistoryCleared();
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Couldn't find current fragment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +113,7 @@ public class HistoryActivity extends AppCompatActivity {
|
||||||
fragment = SearchHistoryFragment.newInstance();
|
fragment = SearchHistoryFragment.newInstance();
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
fragment = WatchedHistoryFragment.newInstance();
|
fragment = WatchHistoryFragment.newInstance();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("position: " + position);
|
throw new IllegalArgumentException("position: " + position);
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package org.schabi.newpipe.history;
|
package org.schabi.newpipe.history;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -19,19 +19,20 @@ import java.util.Date;
|
||||||
* @param <E> the type of the entries
|
* @param <E> the type of the entries
|
||||||
* @param <VH> the type of the view holder
|
* @param <VH> the type of the view holder
|
||||||
*/
|
*/
|
||||||
public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
public abstract class HistoryEntryAdapter<E, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||||
|
|
||||||
private final ArrayList<E> mEntries;
|
private final ArrayList<E> mEntries;
|
||||||
private final DateFormat mDateFormat;
|
private final DateFormat mDateFormat;
|
||||||
|
private final Context mContext;
|
||||||
private OnHistoryItemClickListener<E> onHistoryItemClickListener = null;
|
private OnHistoryItemClickListener<E> onHistoryItemClickListener = null;
|
||||||
|
|
||||||
|
|
||||||
public HistoryEntryAdapter(Context context) {
|
public HistoryEntryAdapter(Context context) {
|
||||||
super();
|
super();
|
||||||
|
mContext = context;
|
||||||
mEntries = new ArrayList<>();
|
mEntries = new ArrayList<>();
|
||||||
mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext());
|
mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM,
|
||||||
|
Localization.getPreferredLocale(context));
|
||||||
setHasStableIds(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEntries(@NonNull Collection<E> historyEntries) {
|
public void setEntries(@NonNull Collection<E> historyEntries) {
|
||||||
|
@ -53,9 +54,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
|
||||||
return mDateFormat.format(date);
|
return mDateFormat.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected String getFormattedViewString(final long viewCount) {
|
||||||
public long getItemId(int position) {
|
return Localization.shortViewCount(mContext, viewCount);
|
||||||
return mEntries.get(position).getId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,15 +66,20 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(VH holder, int position) {
|
public void onBindViewHolder(VH holder, int position) {
|
||||||
final E entry = mEntries.get(position);
|
final E entry = mEntries.get(position);
|
||||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
@Override
|
if(onHistoryItemClickListener != null) {
|
||||||
public void onClick(View v) {
|
onHistoryItemClickListener.onHistoryItemClick(entry);
|
||||||
final OnHistoryItemClickListener<E> historyItemClickListener = onHistoryItemClickListener;
|
|
||||||
if(historyItemClickListener != null) {
|
|
||||||
historyItemClickListener.onHistoryItemClick(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
holder.itemView.setOnLongClickListener(view -> {
|
||||||
|
if (onHistoryItemClickListener != null) {
|
||||||
|
onHistoryItemClickListener.onHistoryItemLongClick(entry);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
onBindViewHolder(holder, entry, position);
|
onBindViewHolder(holder, entry, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,13 +99,8 @@ public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends Rec
|
||||||
return mEntries.isEmpty();
|
return mEntries.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public E removeItemAt(int position) {
|
public interface OnHistoryItemClickListener<E> {
|
||||||
E entry = mEntries.remove(position);
|
void onHistoryItemClick(E item);
|
||||||
notifyItemRemoved(position);
|
void onHistoryItemLongClick(E item);
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnHistoryItemClickListener<E extends HistoryEntry> {
|
|
||||||
void onHistoryItemClick(E historyItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.schabi.newpipe.history;
|
||||||
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
@ -12,34 +11,33 @@ import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.reactivestreams.Subscriber;
|
||||||
|
import org.reactivestreams.Subscription;
|
||||||
import org.schabi.newpipe.BaseFragment;
|
import org.schabi.newpipe.BaseFragment;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.model.HistoryEntry;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Observer;
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Single;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import io.reactivex.subjects.PublishSubject;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragment
|
public abstract class HistoryFragment<E> extends BaseFragment
|
||||||
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
|
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
|
||||||
|
|
||||||
private SharedPreferences mSharedPreferences;
|
private SharedPreferences mSharedPreferences;
|
||||||
|
@ -54,12 +52,11 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
Parcelable mRecyclerViewState;
|
Parcelable mRecyclerViewState;
|
||||||
private RecyclerView mRecyclerView;
|
private RecyclerView mRecyclerView;
|
||||||
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
|
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
|
||||||
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
|
|
||||||
// private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
|
|
||||||
|
|
||||||
private HistoryDAO<E> mHistoryDataSource;
|
private Subscription historySubscription;
|
||||||
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
|
|
||||||
private PublishSubject<Collection<E>> mHistoryEntryInsertSubject;
|
protected HistoryRecordManager historyRecordManager;
|
||||||
|
protected CompositeDisposable disposables;
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
abstract int getEnabledConfigKey();
|
abstract int getEnabledConfigKey();
|
||||||
|
@ -77,88 +74,47 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
// Register history enabled listener
|
// Register history enabled listener
|
||||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||||
|
|
||||||
mHistoryDataSource = createHistoryDAO();
|
historyRecordManager = new HistoryRecordManager(getContext());
|
||||||
|
disposables = new CompositeDisposable();
|
||||||
mHistoryEntryDeleteSubject = PublishSubject.create();
|
|
||||||
mHistoryEntryDeleteSubject
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe(new Consumer<Collection<E>>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Collection<E> historyEntries) throws Exception {
|
|
||||||
mHistoryDataSource.delete(historyEntries);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mHistoryEntryInsertSubject = PublishSubject.create();
|
|
||||||
mHistoryEntryInsertSubject
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe(new Consumer<Collection<E>>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Collection<E> historyEntries) throws Exception {
|
|
||||||
mHistoryDataSource.insertAll(historyEntries);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void historyItemSwipeCallback(int swipeDirection) {
|
|
||||||
mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, swipeDirection) {
|
|
||||||
@Override
|
|
||||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
|
|
||||||
if (mHistoryAdapter != null) {
|
|
||||||
final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition());
|
|
||||||
mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry));
|
|
||||||
|
|
||||||
View view = getActivity().findViewById(R.id.main_content);
|
|
||||||
if (view == null) view = mRecyclerView.getRootView();
|
|
||||||
|
|
||||||
Snackbar.make(view, R.string.item_deleted, 5 * 1000)
|
|
||||||
.setActionTextColor(Color.WHITE)
|
|
||||||
.setAction(R.string.undo, new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry));
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
|
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
|
||||||
|
|
||||||
|
protected abstract Single<List<Long>> insert(final Collection<E> entries);
|
||||||
|
|
||||||
|
protected abstract Single<Integer> delete(final Collection<E> entries);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected abstract Flowable<List<E>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
mHistoryDataSource.getAll()
|
|
||||||
.toObservable()
|
getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber());
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(getHistoryListConsumer());
|
final boolean newEnabled = isHistoryEnabled();
|
||||||
boolean newEnabled = isHistoryEnabled();
|
|
||||||
if (newEnabled != mHistoryIsEnabled) {
|
if (newEnabled != mHistoryIsEnabled) {
|
||||||
onHistoryIsEnabledChanged(newEnabled);
|
onHistoryIsEnabledChanged(newEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Observer<List<E>> getHistoryListConsumer() {
|
private Subscriber<List<E>> getHistorySubscriber() {
|
||||||
return new Observer<List<E>>() {
|
return new Subscriber<List<E>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@NonNull Disposable d) {
|
public void onSubscribe(Subscription s) {
|
||||||
|
if (historySubscription != null) historySubscription.cancel();
|
||||||
|
|
||||||
|
historySubscription = s;
|
||||||
|
historySubscription.request(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(@NonNull List<E> historyEntries) {
|
public void onNext(List<E> entries) {
|
||||||
if (!historyEntries.isEmpty()) {
|
if (!entries.isEmpty()) {
|
||||||
mHistoryAdapter.setEntries(historyEntries);
|
mHistoryAdapter.setEntries(entries);
|
||||||
animateView(mEmptyHistoryView, false, 200);
|
animateView(mEmptyHistoryView, false, 200);
|
||||||
|
|
||||||
if (mRecyclerViewState != null) {
|
if (mRecyclerViewState != null) {
|
||||||
|
@ -169,11 +125,13 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
mHistoryAdapter.clear();
|
mHistoryAdapter.clear();
|
||||||
showEmptyHistory();
|
showEmptyHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (historySubscription != null) historySubscription.request(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(@NonNull Throwable e) {
|
public void onError(Throwable t) {
|
||||||
// TODO: error handling like in (see e.g. subscription fragment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -192,30 +150,48 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
public void onHistoryCleared() {
|
public void onHistoryCleared() {
|
||||||
final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState();
|
if (getContext() == null) return;
|
||||||
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
|
|
||||||
mHistoryEntryDeleteSubject.onNext(itemsToDelete);
|
new AlertDialog.Builder(getContext())
|
||||||
|
.setTitle(R.string.delete_all)
|
||||||
|
.setMessage(R.string.delete_all_history_prompt)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.delete_all, (dialog, i) -> clearHistory())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void makeSnackbar(@StringRes final int text) {
|
||||||
|
if (getActivity() == null) return;
|
||||||
|
|
||||||
View view = getActivity().findViewById(R.id.main_content);
|
View view = getActivity().findViewById(R.id.main_content);
|
||||||
if (view == null) view = mRecyclerView.getRootView();
|
if (view == null) view = mRecyclerView.getRootView();
|
||||||
|
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
|
||||||
if (!itemsToDelete.isEmpty()) {
|
|
||||||
Snackbar.make(view, R.string.history_cleared, 5 * 1000)
|
|
||||||
.setActionTextColor(Color.WHITE)
|
|
||||||
.setAction(R.string.undo, new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
mRecyclerViewState = stateBeforeClear;
|
|
||||||
mHistoryEntryInsertSubject.onNext(itemsToDelete);
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
} else {
|
|
||||||
Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearHistory() {
|
||||||
|
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
|
||||||
|
|
||||||
|
final Disposable deletion = delete(itemsToDelete)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> Log.d(TAG, "Clear history deleted [" +
|
||||||
|
itemsToDelete.size() + "] items."),
|
||||||
|
error -> Log.e(TAG, "Clear history delete step failed", error)
|
||||||
|
);
|
||||||
|
|
||||||
|
final Disposable cleanUp = historyRecordManager.removeOrphanedRecords()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"),
|
||||||
|
error -> Log.e(TAG, "Clear history remove orphaned records failed", error)
|
||||||
|
);
|
||||||
|
|
||||||
|
disposables.addAll(deletion, cleanUp);
|
||||||
|
|
||||||
|
makeSnackbar(R.string.history_cleared);
|
||||||
mHistoryAdapter.clear();
|
mHistoryAdapter.clear();
|
||||||
showEmptyHistory();
|
showEmptyHistory();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showEmptyHistory() {
|
private void showEmptyHistory() {
|
||||||
|
@ -227,18 +203,18 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
@Nullable
|
@Nullable
|
||||||
@CallSuper
|
@CallSuper
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
|
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
|
||||||
mRecyclerView = rootView.findViewById(R.id.history_view);
|
mRecyclerView = rootView.findViewById(R.id.history_view);
|
||||||
|
|
||||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(),
|
||||||
|
LinearLayoutManager.VERTICAL, false);
|
||||||
mRecyclerView.setLayoutManager(layoutManager);
|
mRecyclerView.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
mHistoryAdapter = createAdapter();
|
mHistoryAdapter = createAdapter();
|
||||||
mHistoryAdapter.setOnHistoryItemClickListener(this);
|
mHistoryAdapter.setOnHistoryItemClickListener(this);
|
||||||
mRecyclerView.setAdapter(mHistoryAdapter);
|
mRecyclerView.setAdapter(mHistoryAdapter);
|
||||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback);
|
|
||||||
itemTouchHelper.attachToRecyclerView(mRecyclerView);
|
|
||||||
mDisabledView = rootView.findViewById(R.id.history_disabled_view);
|
mDisabledView = rootView.findViewById(R.id.history_disabled_view);
|
||||||
mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
|
mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
|
||||||
|
|
||||||
|
@ -256,11 +232,16 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
|
if (disposables != null) disposables.dispose();
|
||||||
|
if (historySubscription != null) historySubscription.cancel();
|
||||||
|
|
||||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||||
mSharedPreferences = null;
|
mSharedPreferences = null;
|
||||||
mHistoryIsEnabledChangeListener = null;
|
mHistoryIsEnabledChangeListener = null;
|
||||||
mHistoryIsEnabledKey = null;
|
mHistoryIsEnabledKey = null;
|
||||||
mHistoryDataSource = null;
|
historySubscription = null;
|
||||||
|
disposables = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -290,15 +271,8 @@ public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private class HistoryIsEnabledChangeListener
|
||||||
* Creates a new history DAO
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
*
|
|
||||||
* @return the history DAO
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
protected abstract HistoryDAO<E> createHistoryDAO();
|
|
||||||
|
|
||||||
private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
if (key.equals(mHistoryIsEnabledKey)) {
|
if (key.equals(mHistoryIsEnabledKey)) {
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
package org.schabi.newpipe.history;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
|
||||||
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamStateDAO;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Maybe;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
public class HistoryRecordManager {
|
||||||
|
|
||||||
|
private final AppDatabase database;
|
||||||
|
private final StreamDAO streamTable;
|
||||||
|
private final StreamHistoryDAO streamHistoryTable;
|
||||||
|
private final SearchHistoryDAO searchHistoryTable;
|
||||||
|
private final StreamStateDAO streamStateTable;
|
||||||
|
private final SharedPreferences sharedPreferences;
|
||||||
|
private final String searchHistoryKey;
|
||||||
|
private final String streamHistoryKey;
|
||||||
|
|
||||||
|
public HistoryRecordManager(final Context context) {
|
||||||
|
database = NewPipeDatabase.getInstance(context);
|
||||||
|
streamTable = database.streamDAO();
|
||||||
|
streamHistoryTable = database.streamHistoryDAO();
|
||||||
|
searchHistoryTable = database.searchHistoryDAO();
|
||||||
|
streamStateTable = database.streamStateDAO();
|
||||||
|
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
searchHistoryKey = context.getString(R.string.enable_search_history_key);
|
||||||
|
streamHistoryKey = context.getString(R.string.enable_watch_history_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// Watch History
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public Maybe<Long> onViewed(final StreamInfo info) {
|
||||||
|
if (!isStreamHistoryEnabled()) return Maybe.empty();
|
||||||
|
|
||||||
|
final Date currentTime = new Date();
|
||||||
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
|
StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry();
|
||||||
|
|
||||||
|
if (latestEntry != null && latestEntry.getStreamUid() == streamId) {
|
||||||
|
streamHistoryTable.delete(latestEntry);
|
||||||
|
latestEntry.setAccessDate(currentTime);
|
||||||
|
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
|
||||||
|
return streamHistoryTable.insert(latestEntry);
|
||||||
|
} else {
|
||||||
|
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deleteStreamHistory(final long streamId) {
|
||||||
|
return Single.fromCallable(() -> streamHistoryTable.deleteStreamHistory(streamId))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
|
||||||
|
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<StreamStatisticsEntry>> getStreamStatistics() {
|
||||||
|
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) {
|
||||||
|
List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
|
||||||
|
for (final StreamHistoryEntry entry : entries) {
|
||||||
|
entities.add(entry.toStreamHistoryEntity());
|
||||||
|
}
|
||||||
|
return Single.fromCallable(() -> streamHistoryTable.insertAll(entities))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) {
|
||||||
|
List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
|
||||||
|
for (final StreamHistoryEntry entry : entries) {
|
||||||
|
entities.add(entry.toStreamHistoryEntity());
|
||||||
|
}
|
||||||
|
return Single.fromCallable(() -> streamHistoryTable.delete(entities))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStreamHistoryEnabled() {
|
||||||
|
return sharedPreferences.getBoolean(streamHistoryKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// Search History
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public Single<List<Long>> insertSearches(final Collection<SearchHistoryEntry> entries) {
|
||||||
|
return Single.fromCallable(() -> searchHistoryTable.insertAll(entries))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deleteSearches(final Collection<SearchHistoryEntry> entries) {
|
||||||
|
return Single.fromCallable(() -> searchHistoryTable.delete(entries))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<SearchHistoryEntry>> getSearchHistory() {
|
||||||
|
return searchHistoryTable.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<Long> onSearched(final int serviceId, final String search) {
|
||||||
|
if (!isSearchHistoryEnabled()) return Maybe.empty();
|
||||||
|
|
||||||
|
final Date currentTime = new Date();
|
||||||
|
final SearchHistoryEntry newEntry = new SearchHistoryEntry(currentTime, serviceId, search);
|
||||||
|
|
||||||
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
|
SearchHistoryEntry latestEntry = searchHistoryTable.getLatestEntry();
|
||||||
|
if (latestEntry != null && latestEntry.hasEqualValues(newEntry)) {
|
||||||
|
latestEntry.setCreationDate(currentTime);
|
||||||
|
return (long) searchHistoryTable.update(latestEntry);
|
||||||
|
} else {
|
||||||
|
return searchHistoryTable.insert(newEntry);
|
||||||
|
}
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Single<Integer> deleteSearchHistory(final String search) {
|
||||||
|
return Single.fromCallable(() -> searchHistoryTable.deleteAllWhereQuery(search))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flowable<List<SearchHistoryEntry>> getRelatedSearches(final String query,
|
||||||
|
final int similarQueryLimit,
|
||||||
|
final int uniqueQueryLimit) {
|
||||||
|
return query.length() > 0
|
||||||
|
? searchHistoryTable.getSimilarEntries(query, similarQueryLimit)
|
||||||
|
: searchHistoryTable.getUniqueEntries(uniqueQueryLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSearchHistoryEnabled() {
|
||||||
|
return sharedPreferences.getBoolean(searchHistoryKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// Stream State History
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
|
||||||
|
return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info)))
|
||||||
|
.flatMap(streamId -> streamStateTable.getState(streamId).firstElement())
|
||||||
|
.flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0)))
|
||||||
|
.subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Maybe<Long> saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
|
||||||
|
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
|
||||||
|
final long streamId = streamTable.upsert(new StreamEntity(info));
|
||||||
|
return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime));
|
||||||
|
})).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// Utility
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public Single<Integer> removeOrphanedRecords() {
|
||||||
|
return Single.fromCallable(streamTable::deleteOrphans).subscribeOn(Schedulers.io());
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,22 +5,30 @@ import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
private static int allowedSwipeToDeleteDirections = ItemTouchHelper.RIGHT;
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
|
||||||
|
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static SearchHistoryFragment newInstance() {
|
public static SearchHistoryFragment newInstance() {
|
||||||
|
@ -30,7 +38,6 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -39,38 +46,82 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||||
return new SearchHistoryAdapter(getContext());
|
return new SearchHistoryAdapter(getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<List<Long>> insert(Collection<SearchHistoryEntry> entries) {
|
||||||
|
return historyRecordManager.insertSearches(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<Integer> delete(Collection<SearchHistoryEntry> entries) {
|
||||||
|
return historyRecordManager.deleteSearches(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Flowable<List<SearchHistoryEntry>> getAll() {
|
||||||
|
return historyRecordManager.getSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
@Override
|
@Override
|
||||||
int getEnabledConfigKey() {
|
int getEnabledConfigKey() {
|
||||||
return R.string.enable_search_history_key;
|
return R.string.enable_search_history_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() {
|
public void onHistoryItemClick(final SearchHistoryEntry historyItem) {
|
||||||
return NewPipeDatabase.getInstance().searchHistoryDAO();
|
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(),
|
||||||
|
historyItem.getSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHistoryItemClick(SearchHistoryEntry historyItem) {
|
public void onHistoryItemLongClick(final SearchHistoryEntry item) {
|
||||||
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch());
|
if (activity == null) return;
|
||||||
|
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(item.getSearch())
|
||||||
|
.setMessage(R.string.delete_item_search_history)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNeutralButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
|
||||||
|
final Disposable onDelete = historyRecordManager
|
||||||
|
.deleteSearches(Collections.singleton(item))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/*successful*/},
|
||||||
|
error -> Log.e(TAG, "Search history Delete One failed:", error)
|
||||||
|
);
|
||||||
|
disposables.add(onDelete);
|
||||||
|
makeSnackbar(R.string.item_deleted);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
|
||||||
|
final Disposable onDeleteAll = historyRecordManager
|
||||||
|
.deleteSearchHistory(item.getSearch())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/*successful*/},
|
||||||
|
error -> Log.e(TAG, "Search history Delete All failed:", error)
|
||||||
|
);
|
||||||
|
disposables.add(onDeleteAll);
|
||||||
|
makeSnackbar(R.string.item_deleted);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
private final TextView search;
|
private final TextView search;
|
||||||
private final TextView time;
|
private final TextView info;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
public ViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
search = itemView.findViewById(R.id.search);
|
search = itemView.findViewById(R.id.search);
|
||||||
time = itemView.findViewById(R.id.time);
|
info = itemView.findViewById(R.id.info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
|
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
|
||||||
|
|
||||||
|
SearchHistoryAdapter(Context context) {
|
||||||
public SearchHistoryAdapter(Context context) {
|
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +135,11 @@ public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||||
@Override
|
@Override
|
||||||
void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) {
|
void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) {
|
||||||
holder.search.setText(entry.getSearch());
|
holder.search.setText(entry.getSearch());
|
||||||
holder.time.setText(getFormattedDate(entry.getCreationDate()));
|
|
||||||
|
final String info = Localization.concatenateStrings(
|
||||||
|
getFormattedDate(entry.getCreationDate()),
|
||||||
|
NewPipe.getNameOfService(entry.getServiceId()));
|
||||||
|
holder.info.setText(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
package org.schabi.newpipe.history;
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||||
|
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Flowable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
|
||||||
|
|
||||||
|
public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static WatchHistoryFragment newInstance() {
|
||||||
|
return new WatchHistoryFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
@Override
|
||||||
|
int getEnabledConfigKey() {
|
||||||
|
return R.string.enable_watch_history_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected StreamHistoryAdapter createAdapter() {
|
||||||
|
return new StreamHistoryAdapter(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<List<Long>> insert(Collection<StreamHistoryEntry> entries) {
|
||||||
|
return historyRecordManager.insertStreamHistory(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Single<Integer> delete(Collection<StreamHistoryEntry> entries) {
|
||||||
|
return historyRecordManager.deleteStreamHistory(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Flowable<List<StreamHistoryEntry>> getAll() {
|
||||||
|
return historyRecordManager.getStreamHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHistoryItemClick(StreamHistoryEntry historyItem) {
|
||||||
|
NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url,
|
||||||
|
historyItem.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHistoryItemLongClick(StreamHistoryEntry item) {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(item.title)
|
||||||
|
.setMessage(R.string.delete_stream_history_prompt)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNeutralButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
|
||||||
|
final Disposable onDelete = historyRecordManager
|
||||||
|
.deleteStreamHistory(Collections.singleton(item))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/*successful*/},
|
||||||
|
error -> Log.e(TAG, "Watch history Delete One failed:", error)
|
||||||
|
);
|
||||||
|
disposables.add(onDelete);
|
||||||
|
makeSnackbar(R.string.item_deleted);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
|
||||||
|
final Disposable onDeleteAll = historyRecordManager
|
||||||
|
.deleteStreamHistory(item.streamId)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/*successful*/},
|
||||||
|
error -> Log.e(TAG, "Watch history Delete All failed:", error)
|
||||||
|
);
|
||||||
|
disposables.add(onDeleteAll);
|
||||||
|
makeSnackbar(R.string.item_deleted);
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StreamHistoryAdapter extends HistoryEntryAdapter<StreamHistoryEntry, ViewHolder> {
|
||||||
|
|
||||||
|
StreamHistoryAdapter(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||||
|
View itemView = inflater.inflate(R.layout.list_stream_item, parent, false);
|
||||||
|
return new ViewHolder(itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(ViewHolder holder) {
|
||||||
|
holder.itemView.setOnClickListener(null);
|
||||||
|
ImageLoader.getInstance()
|
||||||
|
.cancelDisplayTask(holder.thumbnailView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) {
|
||||||
|
final String formattedDate = getFormattedDate(entry.accessDate);
|
||||||
|
final String info;
|
||||||
|
if (entry.repeatCount > 1) {
|
||||||
|
info = Localization.concatenateStrings(formattedDate,
|
||||||
|
getFormattedViewString(entry.repeatCount));
|
||||||
|
} else {
|
||||||
|
info = formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.info.setText(info);
|
||||||
|
holder.streamTitle.setText(entry.title);
|
||||||
|
holder.uploader.setText(entry.uploader);
|
||||||
|
holder.duration.setText(Localization.getDurationString(entry.duration));
|
||||||
|
ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
|
||||||
|
StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private final TextView info;
|
||||||
|
private final TextView streamTitle;
|
||||||
|
private final ImageView thumbnailView;
|
||||||
|
private final TextView uploader;
|
||||||
|
private final TextView duration;
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
thumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
info = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||||
|
streamTitle = itemView.findViewById(R.id.itemVideoTitleView);
|
||||||
|
uploader = itemView.findViewById(R.id.itemUploaderView);
|
||||||
|
duration = itemView.findViewById(R.id.itemDurationView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,116 +0,0 @@
|
||||||
package org.schabi.newpipe.history;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.annotation.StringRes;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.database.history.dao.HistoryDAO;
|
|
||||||
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
|
|
||||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
|
|
||||||
|
|
||||||
public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
|
|
||||||
|
|
||||||
private static int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static WatchedHistoryFragment newInstance() {
|
|
||||||
return new WatchedHistoryFragment();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
historyItemSwipeCallback(allowedSwipeToDeleteDirections);
|
|
||||||
}
|
|
||||||
|
|
||||||
@StringRes
|
|
||||||
@Override
|
|
||||||
int getEnabledConfigKey() {
|
|
||||||
return R.string.enable_watch_history_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
protected WatchedHistoryAdapter createAdapter() {
|
|
||||||
return new WatchedHistoryAdapter(getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() {
|
|
||||||
return NewPipeDatabase.getInstance().watchHistoryDAO();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onHistoryItemClick(WatchHistoryEntry historyItem) {
|
|
||||||
NavigationHelper.openVideoDetail(getContext(),
|
|
||||||
historyItem.getServiceId(),
|
|
||||||
historyItem.getUrl(),
|
|
||||||
historyItem.getTitle());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class WatchedHistoryAdapter extends HistoryEntryAdapter<WatchHistoryEntry, ViewHolder> {
|
|
||||||
|
|
||||||
public WatchedHistoryAdapter(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
|
||||||
View itemView = inflater.inflate(R.layout.list_stream_item, parent, false);
|
|
||||||
return new ViewHolder(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewRecycled(ViewHolder holder) {
|
|
||||||
holder.itemView.setOnClickListener(null);
|
|
||||||
ImageLoader.getInstance()
|
|
||||||
.cancelDisplayTask(holder.thumbnailView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) {
|
|
||||||
holder.date.setText(getFormattedDate(entry.getCreationDate()));
|
|
||||||
holder.streamTitle.setText(entry.getTitle());
|
|
||||||
holder.uploader.setText(entry.getUploader());
|
|
||||||
holder.duration.setText(Localization.getDurationString(entry.getDuration()));
|
|
||||||
ImageLoader.getInstance()
|
|
||||||
.displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private final TextView date;
|
|
||||||
private final TextView streamTitle;
|
|
||||||
private final ImageView thumbnailView;
|
|
||||||
private final TextView uploader;
|
|
||||||
private final TextView duration;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
thumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
date = itemView.findViewById(R.id.itemAdditionalDetails);
|
|
||||||
streamTitle = itemView.findViewById(R.id.itemVideoTitleView);
|
|
||||||
uploader = itemView.findViewById(R.id.itemUploaderView);
|
|
||||||
duration = itemView.findViewById(R.id.itemDurationView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,8 +16,10 @@ import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 26.09.16.
|
* Created by Christian Schabesberger on 26.09.16.
|
||||||
|
@ -42,17 +44,12 @@ import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||||
public class InfoItemBuilder {
|
public class InfoItemBuilder {
|
||||||
private static final String TAG = InfoItemBuilder.class.toString();
|
private static final String TAG = InfoItemBuilder.class.toString();
|
||||||
|
|
||||||
public interface OnInfoItemSelectedListener<T extends InfoItem> {
|
|
||||||
void selected(T selectedItem);
|
|
||||||
void held(T selectedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||||
|
|
||||||
private OnInfoItemSelectedListener<StreamInfoItem> onStreamSelectedListener;
|
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
||||||
private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener;
|
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
||||||
private OnInfoItemSelectedListener<PlaylistInfoItem> onPlaylistSelectedListener;
|
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||||
|
|
||||||
public InfoItemBuilder(Context context) {
|
public InfoItemBuilder(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -75,7 +72,7 @@ public class InfoItemBuilder {
|
||||||
case CHANNEL:
|
case CHANNEL:
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
||||||
case PLAYLIST:
|
case PLAYLIST:
|
||||||
return new PlaylistInfoItemHolder(this, parent);
|
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||||
|
@ -90,27 +87,27 @@ public class InfoItemBuilder {
|
||||||
return imageLoader;
|
return imageLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnInfoItemSelectedListener<StreamInfoItem> getOnStreamSelectedListener() {
|
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
||||||
return onStreamSelectedListener;
|
return onStreamSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
|
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
|
||||||
this.onStreamSelectedListener = listener;
|
this.onStreamSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnInfoItemSelectedListener<ChannelInfoItem> getOnChannelSelectedListener() {
|
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
|
||||||
return onChannelSelectedListener;
|
return onChannelSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
|
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
|
||||||
this.onChannelSelectedListener = listener;
|
this.onChannelSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnInfoItemSelectedListener<PlaylistInfoItem> getOnPlaylistSelectedListener() {
|
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
|
||||||
return onPlaylistSelectedListener;
|
return onPlaylistSelectedListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
|
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
|
||||||
this.onPlaylistSelectedListener = listener;
|
this.onPlaylistSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class InfoItemDialog {
|
||||||
@NonNull final StreamInfoItem info,
|
@NonNull final StreamInfoItem info,
|
||||||
@NonNull final String[] commands,
|
@NonNull final String[] commands,
|
||||||
@NonNull final DialogInterface.OnClickListener actions) {
|
@NonNull final DialogInterface.OnClickListener actions) {
|
||||||
this(activity, commands, actions, info.getName(), info.uploader_name);
|
this(activity, commands, actions, info.getName(), info.getUploaderName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public InfoItemDialog(@NonNull final Activity activity,
|
public InfoItemDialog(@NonNull final Activity activity,
|
||||||
|
@ -28,8 +28,7 @@ public class InfoItemDialog {
|
||||||
@NonNull final String title,
|
@NonNull final String title,
|
||||||
@Nullable final String additionalDetail) {
|
@Nullable final String additionalDetail) {
|
||||||
|
|
||||||
final LayoutInflater inflater = activity.getLayoutInflater();
|
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
|
||||||
final View bannerView = inflater.inflate(R.layout.dialog_title, null);
|
|
||||||
bannerView.setSelected(true);
|
bannerView.setSelected(true);
|
||||||
|
|
||||||
TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
||||||
|
|
|
@ -10,13 +10,14 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener;
|
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -52,6 +53,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
private static final int STREAM_HOLDER_TYPE = 0x101;
|
private static final int STREAM_HOLDER_TYPE = 0x101;
|
||||||
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
|
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
|
||||||
private static final int CHANNEL_HOLDER_TYPE = 0x201;
|
private static final int CHANNEL_HOLDER_TYPE = 0x201;
|
||||||
|
private static final int MINI_PLAYLIST_HOLDER_TYPE = 0x300;
|
||||||
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
|
||||||
|
|
||||||
private final InfoItemBuilder infoItemBuilder;
|
private final InfoItemBuilder infoItemBuilder;
|
||||||
|
@ -75,15 +77,15 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
infoItemList = new ArrayList<>();
|
infoItemList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
|
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
|
||||||
infoItemBuilder.setOnStreamSelectedListener(listener);
|
infoItemBuilder.setOnStreamSelectedListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
|
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
|
||||||
infoItemBuilder.setOnChannelSelectedListener(listener);
|
infoItemBuilder.setOnChannelSelectedListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
|
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
|
||||||
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,14 +202,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
if (footer != null && position == infoItemList.size() && showFooter) {
|
if (footer != null && position == infoItemList.size() && showFooter) {
|
||||||
return FOOTER_TYPE;
|
return FOOTER_TYPE;
|
||||||
}
|
}
|
||||||
InfoItem item = infoItemList.get(position);
|
final InfoItem item = infoItemList.get(position);
|
||||||
switch (item.info_type) {
|
switch (item.info_type) {
|
||||||
case STREAM:
|
case STREAM:
|
||||||
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||||
case CHANNEL:
|
case CHANNEL:
|
||||||
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||||
case PLAYLIST:
|
case PLAYLIST:
|
||||||
return PLAYLIST_HOLDER_TYPE;
|
return useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Trollolo");
|
Log.e(TAG, "Trollolo");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -230,6 +232,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||||
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case CHANNEL_HOLDER_TYPE:
|
case CHANNEL_HOLDER_TYPE:
|
||||||
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
return new ChannelInfoItemHolder(infoItemBuilder, parent);
|
||||||
|
case MINI_PLAYLIST_HOLDER_TYPE:
|
||||||
|
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
|
||||||
case PLAYLIST_HOLDER_TYPE:
|
case PLAYLIST_HOLDER_TYPE:
|
||||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,62 +1,13 @@
|
||||||
package org.schabi.newpipe.info_list.holder;
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
|
||||||
public class PlaylistInfoItemHolder extends InfoItemHolder {
|
public class PlaylistInfoItemHolder extends PlaylistMiniInfoItemHolder {
|
||||||
public final ImageView itemThumbnailView;
|
|
||||||
public final TextView itemStreamCountView;
|
|
||||||
public final TextView itemTitleView;
|
|
||||||
public final TextView itemUploaderView;
|
|
||||||
|
|
||||||
public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
super(infoItemBuilder, R.layout.list_playlist_item, parent);
|
super(infoItemBuilder, R.layout.list_playlist_item, parent);
|
||||||
|
|
||||||
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
|
||||||
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
|
||||||
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
|
|
||||||
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFromItem(final InfoItem infoItem) {
|
|
||||||
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
|
||||||
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getName());
|
|
||||||
itemStreamCountView.setText(item.stream_count + "");
|
|
||||||
itemUploaderView.setText(item.uploader_name);
|
|
||||||
|
|
||||||
itemBuilder.getImageLoader()
|
|
||||||
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
|
||||||
|
|
||||||
itemView.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
|
||||||
itemBuilder.getOnPlaylistSelectedListener().selected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display options for playlist thumbnails
|
|
||||||
*/
|
|
||||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
|
||||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
|
||||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
|
||||||
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
public final ImageView itemThumbnailView;
|
||||||
|
public final TextView itemStreamCountView;
|
||||||
|
public final TextView itemTitleView;
|
||||||
|
public final TextView itemUploaderView;
|
||||||
|
|
||||||
|
public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
|
||||||
|
super(infoItemBuilder, layoutId, parent);
|
||||||
|
|
||||||
|
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||||
|
itemTitleView = itemView.findViewById(R.id.itemTitleView);
|
||||||
|
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
|
||||||
|
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||||
|
this(infoItemBuilder, R.layout.list_playlist_mini_item, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFromItem(final InfoItem infoItem) {
|
||||||
|
if (!(infoItem instanceof PlaylistInfoItem)) return;
|
||||||
|
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
|
||||||
|
|
||||||
|
itemTitleView.setText(item.getName());
|
||||||
|
itemStreamCountView.setText(String.valueOf(item.getStreamCount()));
|
||||||
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
|
itemBuilder.getImageLoader()
|
||||||
|
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnPlaylistSelectedListener().selected(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemView.setLongClickable(true);
|
||||||
|
itemView.setOnLongClickListener(view -> {
|
||||||
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
itemBuilder.getOnPlaylistSelectedListener().held(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display options for playlist thumbnails
|
||||||
|
*/
|
||||||
|
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||||
|
new DisplayImageOptions.Builder()
|
||||||
|
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||||
|
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||||
|
.build();
|
||||||
|
}
|
|
@ -63,6 +63,7 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||||
import org.schabi.newpipe.player.helper.CacheFactory;
|
import org.schabi.newpipe.player.helper.CacheFactory;
|
||||||
import org.schabi.newpipe.player.helper.LoadController;
|
import org.schabi.newpipe.player.helper.LoadController;
|
||||||
|
@ -77,9 +78,8 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.functions.Predicate;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||||
|
|
||||||
|
@ -147,6 +147,9 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
protected DefaultExtractorsFactory extractorsFactory;
|
protected DefaultExtractorsFactory extractorsFactory;
|
||||||
|
|
||||||
protected Disposable progressUpdateReactor;
|
protected Disposable progressUpdateReactor;
|
||||||
|
protected CompositeDisposable databaseUpdateReactor;
|
||||||
|
|
||||||
|
protected HistoryRecordManager recordManager;
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@ -172,6 +175,10 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
public void initPlayer() {
|
public void initPlayer() {
|
||||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||||
|
|
||||||
|
if (recordManager == null) recordManager = new HistoryRecordManager(context);
|
||||||
|
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||||
|
databaseUpdateReactor = new CompositeDisposable();
|
||||||
|
|
||||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
final AdaptiveTrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||||
final LoadControl loadControl = new LoadController(context);
|
final LoadControl loadControl = new LoadController(context);
|
||||||
|
@ -193,18 +200,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
private Disposable getProgressReactor() {
|
private Disposable getProgressReactor() {
|
||||||
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
|
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.filter(new Predicate<Long>() {
|
.filter(ignored -> isProgressLoopRunning())
|
||||||
@Override
|
.subscribe(ignored -> triggerProgressUpdate());
|
||||||
public boolean test(Long aLong) throws Exception {
|
|
||||||
return isProgressLoopRunning();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.subscribe(new Consumer<Long>() {
|
|
||||||
@Override
|
|
||||||
public void accept(Long aLong) throws Exception {
|
|
||||||
triggerProgressUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleIntent(Intent intent) {
|
public void handleIntent(Intent intent) {
|
||||||
|
@ -281,6 +278,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (playQueue != null) playQueue.dispose();
|
if (playQueue != null) playQueue.dispose();
|
||||||
if (playbackManager != null) playbackManager.dispose();
|
if (playbackManager != null) playbackManager.dispose();
|
||||||
if (audioReactor != null) audioReactor.abandonAudioFocus();
|
if (audioReactor != null) audioReactor.abandonAudioFocus();
|
||||||
|
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
|
@ -291,6 +289,7 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
|
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
simpleExoPlayer = null;
|
simpleExoPlayer = null;
|
||||||
|
recordManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
public MediaSource buildMediaSource(String url, String overrideExtension) {
|
||||||
|
@ -582,6 +581,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
errorToast = null;
|
errorToast = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
savePlaybackState();
|
||||||
|
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case ExoPlaybackException.TYPE_SOURCE:
|
case ExoPlaybackException.TYPE_SOURCE:
|
||||||
if (simpleExoPlayer.getCurrentPosition() <
|
if (simpleExoPlayer.getCurrentPosition() <
|
||||||
|
@ -612,7 +613,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
// If the user selects a new track, then the discontinuity occurs after the index is changed.
|
// If the user selects a new track, then the discontinuity occurs after the index is changed.
|
||||||
// Therefore, the only source that causes a discrepancy would be gapless transition,
|
// Therefore, the only source that causes a discrepancy would be gapless transition,
|
||||||
// which can only offset the current track by +1.
|
// which can only offset the current track by +1.
|
||||||
if (newWindowIndex == playQueue.getIndex() + 1) {
|
if (newWindowIndex == playQueue.getIndex() + 1 ||
|
||||||
|
(newWindowIndex == 0 && playQueue.getIndex() == playQueue.size() - 1)) {
|
||||||
playQueue.offsetIndex(+1);
|
playQueue.offsetIndex(+1);
|
||||||
}
|
}
|
||||||
playbackManager.load();
|
playbackManager.load();
|
||||||
|
@ -668,10 +670,17 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
"], queue index=[" + playQueue.getIndex() + "]");
|
"], queue index=[" + playQueue.getIndex() + "]");
|
||||||
} else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
|
} else if (simpleExoPlayer.getCurrentWindowIndex() != currentSourceIndex || !isPlaying()) {
|
||||||
final long startPos = info != null ? info.start_position : 0;
|
final long startPos = info != null ? info.start_position : 0;
|
||||||
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
|
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex +
|
||||||
|
" at: " + getTimeString((int)startPos));
|
||||||
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: update exoplayer to 2.6.x in order to register view count on repeated streams
|
||||||
|
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/* successful */},
|
||||||
|
error -> Log.e(TAG, "Player onViewed() failure: ", error)
|
||||||
|
));
|
||||||
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
initThumbnail(info == null ? item.getThumbnailUrl() : info.thumbnail_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -755,6 +764,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (simpleExoPlayer == null || playQueue == null) return;
|
if (simpleExoPlayer == null || playQueue == null) return;
|
||||||
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
||||||
|
|
||||||
|
savePlaybackState();
|
||||||
|
|
||||||
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
|
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds, restart current track.
|
||||||
* Also restart the track if the current track is the first in a queue.*/
|
* Also restart the track if the current track is the first in a queue.*/
|
||||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
|
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT || playQueue.getIndex() == 0) {
|
||||||
|
@ -769,6 +780,8 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
if (playQueue == null) return;
|
if (playQueue == null) return;
|
||||||
if (DEBUG) Log.d(TAG, "onPlayNext() called");
|
if (DEBUG) Log.d(TAG, "onPlayNext() called");
|
||||||
|
|
||||||
|
savePlaybackState();
|
||||||
|
|
||||||
playQueue.offsetIndex(+1);
|
playQueue.offsetIndex(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -830,6 +843,27 @@ public abstract class BasePlayer implements Player.EventListener, PlaybackListen
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void savePlaybackState(final StreamInfo info, final long progress) {
|
||||||
|
if (context == null || info == null || databaseUpdateReactor == null) return;
|
||||||
|
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.onErrorComplete()
|
||||||
|
.subscribe(
|
||||||
|
ignored -> {/* successful */},
|
||||||
|
error -> Log.e(TAG, "savePlaybackState() failure: ", error)
|
||||||
|
);
|
||||||
|
databaseUpdateReactor.add(stateSaver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePlaybackState() {
|
||||||
|
if (simpleExoPlayer == null || currentInfo == null) return;
|
||||||
|
|
||||||
|
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD &&
|
||||||
|
simpleExoPlayer.getCurrentPosition() <
|
||||||
|
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
|
||||||
|
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Player;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
|
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||||
|
@ -149,8 +150,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
case android.R.id.home:
|
case android.R.id.home:
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_history:
|
case R.id.action_append_playlist:
|
||||||
NavigationHelper.openHistory(this);
|
appendToPlaylist();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_settings:
|
case R.id.action_settings:
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
|
@ -185,6 +186,14 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
null
|
null
|
||||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendToPlaylist() {
|
||||||
|
if (this.player != null && this.player.getPlayQueue() != null) {
|
||||||
|
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
|
||||||
|
.show(getSupportFragmentManager(), getTag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Service Connection
|
// Service Connection
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -23,6 +24,7 @@ public class PlayQueueItem implements Serializable {
|
||||||
final private long duration;
|
final private long duration;
|
||||||
final private String thumbnailUrl;
|
final private String thumbnailUrl;
|
||||||
final private String uploader;
|
final private String uploader;
|
||||||
|
final private StreamType streamType;
|
||||||
|
|
||||||
private long recoveryPosition;
|
private long recoveryPosition;
|
||||||
private Throwable error;
|
private Throwable error;
|
||||||
|
@ -30,22 +32,26 @@ public class PlayQueueItem implements Serializable {
|
||||||
private transient Single<StreamInfo> stream;
|
private transient Single<StreamInfo> stream;
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.duration, info.thumbnail_url, info.uploader_name);
|
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||||
|
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
||||||
this.stream = Single.just(info);
|
this.stream = Single.just(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||||
this(item.getName(), item.getUrl(), item.getServiceId(), item.duration, item.thumbnail_url, item.uploader_name);
|
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
|
||||||
|
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueueItem(final String name, final String url, final int serviceId,
|
private PlayQueueItem(final String name, final String url, final int serviceId,
|
||||||
final long duration, final String thumbnailUrl, final String uploader) {
|
final long duration, final String thumbnailUrl, final String uploader,
|
||||||
|
final StreamType streamType) {
|
||||||
this.title = name;
|
this.title = name;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.thumbnailUrl = thumbnailUrl;
|
this.thumbnailUrl = thumbnailUrl;
|
||||||
this.uploader = uploader;
|
this.uploader = uploader;
|
||||||
|
this.streamType = streamType;
|
||||||
|
|
||||||
this.recoveryPosition = RECOVERY_UNSET;
|
this.recoveryPosition = RECOVERY_UNSET;
|
||||||
}
|
}
|
||||||
|
@ -78,6 +84,10 @@ public class PlayQueueItem implements Serializable {
|
||||||
return uploader;
|
return uploader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StreamType getStreamType() {
|
||||||
|
return streamType;
|
||||||
|
}
|
||||||
|
|
||||||
public long getRecoveryPosition() {
|
public long getRecoveryPosition() {
|
||||||
return recoveryPosition;
|
return recoveryPosition;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,29 @@ package org.schabi.newpipe.playlist;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class SinglePlayQueue extends PlayQueue {
|
public final class SinglePlayQueue extends PlayQueue {
|
||||||
public SinglePlayQueue(final StreamInfoItem item) {
|
public SinglePlayQueue(final StreamInfoItem item) {
|
||||||
this(new PlayQueueItem(item));
|
super(0, Collections.singletonList(new PlayQueueItem(item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SinglePlayQueue(final StreamInfo info) {
|
public SinglePlayQueue(final StreamInfo info) {
|
||||||
this(new PlayQueueItem(info));
|
super(0, Collections.singletonList(new PlayQueueItem(info)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private SinglePlayQueue(final PlayQueueItem playQueueItem) {
|
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
|
||||||
super(0, Collections.singletonList(playQueueItem));
|
super(index, playQueueItemsOf(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PlayQueueItem> playQueueItemsOf(List<StreamInfoItem> items) {
|
||||||
|
List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
|
||||||
|
for (final StreamInfoItem item : items) {
|
||||||
|
playQueueItems.add(new PlayQueueItem(item));
|
||||||
|
}
|
||||||
|
return playQueueItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.PluralsRes;
|
import android.support.annotation.PluralsRes;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -14,7 +15,9 @@ import java.text.DateFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -39,9 +42,33 @@ import java.util.Locale;
|
||||||
|
|
||||||
public class Localization {
|
public class Localization {
|
||||||
|
|
||||||
|
public final static String DOT_SEPARATOR = " • ";
|
||||||
|
|
||||||
private Localization() {
|
private Localization() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String concatenateStrings(final String... strings) {
|
||||||
|
return concatenateStrings(Arrays.asList(strings));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String concatenateStrings(final List<String> strings) {
|
||||||
|
if (strings.isEmpty()) return "";
|
||||||
|
|
||||||
|
final StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
stringBuilder.append(strings.get(0));
|
||||||
|
|
||||||
|
for (int i = 1; i < strings.size(); i++) {
|
||||||
|
final String string = strings.get(i);
|
||||||
|
if (!TextUtils.isEmpty(string)) {
|
||||||
|
stringBuilder.append(DOT_SEPARATOR).append(strings.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public static Locale getPreferredLocale(Context context) {
|
public static Locale getPreferredLocale(Context context) {
|
||||||
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,9 @@ import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
||||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
|
||||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
|
import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment;
|
||||||
|
import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment;
|
||||||
|
import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment;
|
||||||
import org.schabi.newpipe.history.HistoryActivity;
|
import org.schabi.newpipe.history.HistoryActivity;
|
||||||
import org.schabi.newpipe.player.BackgroundPlayer;
|
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||||
import org.schabi.newpipe.player.BackgroundPlayerActivity;
|
import org.schabi.newpipe.player.BackgroundPlayerActivity;
|
||||||
|
@ -322,6 +325,30 @@ public class NavigationHelper {
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) {
|
||||||
|
if (name == null) name = "";
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
|
||||||
|
.replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name))
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openLastPlayedFragment(FragmentManager fragmentManager) {
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
|
||||||
|
.replace(R.id.fragment_holder, new LastPlayedFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openMostPlayedFragment(FragmentManager fragmentManager) {
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
|
||||||
|
.replace(R.id.fragment_holder, new MostPlayedFragment())
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Through Intents
|
// Through Intents
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
public abstract class OnClickGesture<T> {
|
||||||
|
|
||||||
|
public abstract void selected(T selectedItem);
|
||||||
|
|
||||||
|
public void held(T selectedItem) {
|
||||||
|
// Optional gesture
|
||||||
|
}
|
||||||
|
|
||||||
|
public void drag(T selectedItem, RecyclerView.ViewHolder viewHolder) {
|
||||||
|
// Optional gesture
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/ic_bookmark_black_24dp.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png
Normal file
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-hdpi/ic_playlist_add_black_24dp.png
Normal file
After Width: | Height: | Size: 106 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 159 B |
BIN
app/src/main/res/drawable-hdpi/ic_playlist_add_white_24dp.png
Normal file
After Width: | Height: | Size: 107 B |
BIN
app/src/main/res/drawable-mdpi/ic_bookmark_black_24dp.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
app/src/main/res/drawable-mdpi/ic_bookmark_white_24dp.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
app/src/main/res/drawable-mdpi/ic_playlist_add_black_24dp.png
Normal file
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 122 B |
After Width: | Height: | Size: 124 B |
BIN
app/src/main/res/drawable-mdpi/ic_playlist_add_white_24dp.png
Normal file
After Width: | Height: | Size: 101 B |
BIN
app/src/main/res/drawable-xhdpi/ic_bookmark_black_24dp.png
Normal file
After Width: | Height: | Size: 204 B |
BIN
app/src/main/res/drawable-xhdpi/ic_bookmark_white_24dp.png
Normal file
After Width: | Height: | Size: 213 B |
BIN
app/src/main/res/drawable-xhdpi/ic_playlist_add_black_24dp.png
Normal file
After Width: | Height: | Size: 113 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 163 B |
BIN
app/src/main/res/drawable-xhdpi/ic_playlist_add_white_24dp.png
Normal file
After Width: | Height: | Size: 109 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_bookmark_black_24dp.png
Normal file
After Width: | Height: | Size: 261 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_bookmark_white_24dp.png
Normal file
After Width: | Height: | Size: 273 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_playlist_add_black_24dp.png
Normal file
After Width: | Height: | Size: 129 B |