Merge branch 'qol-updates' of https://github.com/karyogamy/NewPipe into ui
This commit is contained in:
commit
384398a1e2
324 changed files with 17760 additions and 4074 deletions
13
README.md
13
README.md
|
@ -59,18 +59,17 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||||
* Search/Watch Playlists
|
* Search/Watch Playlists
|
||||||
* Watch as queues Playlists
|
* Watch as queues Playlists
|
||||||
* Queuing videos
|
* Queuing videos
|
||||||
|
* Local playlists
|
||||||
|
* Subtitles
|
||||||
|
* Multi-service support (eg. SoundCloud in NewPipe Beta)
|
||||||
|
|
||||||
### Coming Features
|
### Coming Features
|
||||||
|
|
||||||
* Multiservice support (eg. SoundCloud)
|
* Livestream support
|
||||||
* Bookmarks
|
* Cast to UPnP and Cast
|
||||||
* Subtitles support
|
* Show comments
|
||||||
* livestream support
|
|
||||||
* ... and many more
|
* ... and many more
|
||||||
|
|
||||||
### Multiservice support
|
|
||||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||||
The more is done the better it gets!
|
The more is done the better it gets!
|
||||||
|
|
|
@ -8,8 +8,8 @@ android {
|
||||||
applicationId "org.schabi.newpipe"
|
applicationId "org.schabi.newpipe"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 47
|
versionCode 48
|
||||||
versionName "0.11.6"
|
versionName "0.12.0"
|
||||||
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
@ -48,14 +48,20 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
supportLibVersion = '27.0.2'
|
supportLibVersion = '27.1.0'
|
||||||
|
exoPlayerLibVersion = '2.7.1'
|
||||||
|
roomDbLibVersion = '1.0.0'
|
||||||
|
leakCanaryLibVersion = '1.5.4'
|
||||||
|
okHttpLibVersion = '1.5.0'
|
||||||
|
icepickLibVersion = '3.2.0'
|
||||||
|
stethoLibVersion = '1.5.0'
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
|
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
|
||||||
exclude module: 'support-annotations'
|
exclude module: 'support-annotations'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7fd21ec08581d'
|
implementation 'com.github.TeamNewPipe:NewPipeExtractor:f787b375e5fb6d'
|
||||||
|
|
||||||
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'
|
||||||
|
@ -72,21 +78,29 @@ dependencies {
|
||||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||||
implementation 'com.nononsenseapps:filepicker:3.0.1'
|
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:r2.5.4'
|
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
|
||||||
|
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
|
||||||
|
|
||||||
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
|
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
|
||||||
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
|
||||||
debugImplementation 'com.android.support:multidex:1.0.2'
|
debugImplementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.7'
|
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||||
|
|
||||||
implementation 'android.arch.persistence.room:runtime:1.0.0'
|
implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
|
||||||
implementation 'android.arch.persistence.room:rxjava2:1.0.0'
|
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
|
||||||
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
|
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
|
||||||
|
|
||||||
implementation 'frankiesardo:icepick:3.2.0'
|
implementation "frankiesardo:icepick:$icepickLibVersion"
|
||||||
annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
|
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
|
||||||
|
|
||||||
|
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
|
||||||
|
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||||
|
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||||
|
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||||
|
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
|
||||||
}
|
}
|
||||||
|
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
|
@ -35,3 +35,10 @@
|
||||||
@icepick.* <fields>;
|
@icepick.* <fields>;
|
||||||
}
|
}
|
||||||
-keepnames class * { @icepick.State *;}
|
-keepnames class * { @icepick.State *;}
|
||||||
|
|
||||||
|
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp
|
||||||
|
-dontwarn okhttp3.**
|
||||||
|
-dontwarn okio.**
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||||
|
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||||
|
|
|
@ -1,9 +1,26 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.multidex.MultiDex;
|
import android.support.multidex.MultiDex;
|
||||||
|
|
||||||
import com.facebook.stetho.Stetho;
|
import com.facebook.stetho.Stetho;
|
||||||
|
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||||
|
import com.squareup.leakcanary.AndroidHeapDumper;
|
||||||
|
import com.squareup.leakcanary.DefaultLeakDirectoryProvider;
|
||||||
|
import com.squareup.leakcanary.HeapDumper;
|
||||||
|
import com.squareup.leakcanary.LeakCanary;
|
||||||
|
import com.squareup.leakcanary.LeakDirectoryProvider;
|
||||||
|
import com.squareup.leakcanary.RefWatcher;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Downloader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class DebugApp extends App {
|
public class DebugApp extends App {
|
||||||
private static final String TAG = DebugApp.class.toString();
|
private static final String TAG = DebugApp.class.toString();
|
||||||
|
@ -17,10 +34,15 @@ public class DebugApp extends App {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
initStetho();
|
initStetho();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Downloader getDownloader() {
|
||||||
|
return org.schabi.newpipe.Downloader.init(new OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(new StethoInterceptor()));
|
||||||
|
}
|
||||||
|
|
||||||
private void initStetho() {
|
private void initStetho() {
|
||||||
// Create an InitializerBuilder
|
// Create an InitializerBuilder
|
||||||
Stetho.InitializerBuilder initializerBuilder =
|
Stetho.InitializerBuilder initializerBuilder =
|
||||||
|
@ -42,4 +64,41 @@ public class DebugApp extends App {
|
||||||
// Initialize Stetho with the Initializer
|
// Initialize Stetho with the Initializer
|
||||||
Stetho.initialize(initializer);
|
Stetho.initialize(initializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RefWatcher installLeakCanary() {
|
||||||
|
return LeakCanary.refWatcher(this)
|
||||||
|
.heapDumper(new ToggleableHeapDumper(this))
|
||||||
|
// give each object 10 seconds to be gc'ed, before leak canary gets nosy on it
|
||||||
|
.watchDelay(10, TimeUnit.SECONDS)
|
||||||
|
.buildAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ToggleableHeapDumper implements HeapDumper {
|
||||||
|
private final HeapDumper dumper;
|
||||||
|
private final SharedPreferences preferences;
|
||||||
|
private final String dumpingAllowanceKey;
|
||||||
|
|
||||||
|
ToggleableHeapDumper(@NonNull final Context context) {
|
||||||
|
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
|
||||||
|
this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider);
|
||||||
|
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDumpingAllowed() {
|
||||||
|
return preferences.getBoolean(dumpingAllowanceKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File dumpHeap() {
|
||||||
|
return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,12 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".player.old.PlayVideoActivity"
|
android:name=".player.old.PlayVideoActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||||
|
@ -70,6 +76,9 @@
|
||||||
android:name=".history.HistoryActivity"
|
android:name=".history.HistoryActivity"
|
||||||
android:label="@string/title_activity_history"/>
|
android:label="@string/title_activity_history"/>
|
||||||
|
|
||||||
|
<service android:name=".subscription.services.SubscriptionsImportService"/>
|
||||||
|
<service android:name=".subscription.services.SubscriptionsExportService"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PanicResponderActivity"
|
android:name=".PanicResponderActivity"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
@ -117,13 +126,17 @@
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/nnf_provider_paths"/>
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".RouterActivity"
|
android:name=".RouterActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/preferred_player_share_menu_title"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/RouterActivityThemeDark">
|
android:theme="@style/RouterActivityThemeDark">
|
||||||
|
|
||||||
|
<!-- Youtube filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||||
|
@ -172,6 +185,47 @@
|
||||||
<data android:scheme="vnd.youtube"/>
|
<data android:scheme="vnd.youtube"/>
|
||||||
<data android:scheme="vnd.youtube.launch"/>
|
<data android:scheme="vnd.youtube.launch"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Hooktube filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
<data android:host="hooktube.com"/>
|
||||||
|
<data android:host="*.hooktube.com"/>
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/"/>
|
||||||
|
<data android:pathPrefix="/embed/"/>
|
||||||
|
<data android:pathPrefix="/watch"/>
|
||||||
|
<!-- channel prefix -->
|
||||||
|
<data android:pathPrefix="/channel/"/>
|
||||||
|
<data android:pathPrefix="/user/"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Soundcloud filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
<data android:host="soundcloud.com"/>
|
||||||
|
<data android:host="m.soundcloud.com"/>
|
||||||
|
<data android:host="www.soundcloud.com"/>
|
||||||
|
<data android:pathPrefix="/"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Share filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
@ -180,68 +234,7 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".RouterPlayerActivity$FetcherService"
|
android:name=".RouterActivity$FetcherService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".RouterPlayerActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:label="@string/preferred_player_share_menu_title"
|
|
||||||
android:taskAffinity=""
|
|
||||||
android:theme="@style/RouterActivityThemeDark">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
<data android:host="youtube.com"/>
|
|
||||||
<data android:host="m.youtube.com"/>
|
|
||||||
<data android:host="www.youtube.com"/>
|
|
||||||
<!-- video prefix -->
|
|
||||||
<data android:pathPrefix="/v/"/>
|
|
||||||
<data android:pathPrefix="/embed/"/>
|
|
||||||
<data android:pathPrefix="/watch"/>
|
|
||||||
<data android:pathPrefix="/attribution_link"/>
|
|
||||||
<!-- channel prefix -->
|
|
||||||
<data android:pathPrefix="/channel/"/>
|
|
||||||
<data android:pathPrefix="/user/"/>
|
|
||||||
<!-- playlist prefix -->
|
|
||||||
<data android:pathPrefix="/playlist"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
|
||||||
|
|
||||||
<data android:scheme="http"/>
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
<data android:host="youtu.be"/>
|
|
||||||
<data android:pathPrefix="/"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
|
||||||
|
|
||||||
<data android:scheme="vnd.youtube"/>
|
|
||||||
<data android:scheme="vnd.youtube.launch"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND"/>
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<data android:mimeType="text/plain"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,23 +1,25 @@
|
||||||
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.support.annotation.Nullable;
|
||||||
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;
|
||||||
|
import com.squareup.leakcanary.LeakCanary;
|
||||||
|
import com.squareup.leakcanary.RefWatcher;
|
||||||
|
|
||||||
import org.acra.ACRA;
|
import org.acra.ACRA;
|
||||||
import org.acra.config.ACRAConfiguration;
|
import org.acra.config.ACRAConfiguration;
|
||||||
import org.acra.config.ACRAConfigurationException;
|
import org.acra.config.ACRAConfigurationException;
|
||||||
import org.acra.config.ConfigurationBuilder;
|
import org.acra.config.ConfigurationBuilder;
|
||||||
import org.acra.sender.ReportSenderFactory;
|
import org.acra.sender.ReportSenderFactory;
|
||||||
|
import org.schabi.newpipe.extractor.Downloader;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
import org.schabi.newpipe.report.AcraReportSenderFactory;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
@ -29,9 +31,13 @@ import org.schabi.newpipe.util.StateSaver;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import io.reactivex.annotations.NonNull;
|
import io.reactivex.annotations.NonNull;
|
||||||
import io.reactivex.exceptions.CompositeException;
|
import io.reactivex.exceptions.CompositeException;
|
||||||
|
import io.reactivex.exceptions.MissingBackpressureException;
|
||||||
|
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||||
import io.reactivex.exceptions.UndeliverableException;
|
import io.reactivex.exceptions.UndeliverableException;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.plugins.RxJavaPlugins;
|
import io.reactivex.plugins.RxJavaPlugins;
|
||||||
|
@ -56,6 +62,7 @@ import io.reactivex.plugins.RxJavaPlugins;
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
protected static final String TAG = App.class.toString();
|
protected static final String TAG = App.class.toString();
|
||||||
|
private RefWatcher refWatcher;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||||
|
@ -71,54 +78,99 @@ public class App extends Application {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
|
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||||
|
// This process is dedicated to LeakCanary for heap analysis.
|
||||||
|
// You should not init your app in this process.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refWatcher = installLeakCanary();
|
||||||
|
|
||||||
// Initialize settings first because others inits can use its values
|
// Initialize settings first because others inits can use its values
|
||||||
SettingsActivity.initSettings(this);
|
SettingsActivity.initSettings(this);
|
||||||
|
|
||||||
NewPipe.init(Downloader.getInstance());
|
NewPipe.init(getDownloader());
|
||||||
NewPipeDatabase.init(this);
|
|
||||||
StateSaver.init(this);
|
StateSaver.init(this);
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Downloader getDownloader() {
|
||||||
|
return org.schabi.newpipe.Downloader.init(null);
|
||||||
|
}
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
private void configureRxJavaErrorHandler() {
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||||
@Override
|
@Override
|
||||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [" + throwable.getClass().getName() + "]");
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
|
||||||
|
"throwable = [" + throwable.getClass().getName() + "]");
|
||||||
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
if (throwable instanceof UndeliverableException) {
|
||||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
||||||
throwable = throwable.getCause();
|
throwable = throwable.getCause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final List<Throwable> errors;
|
||||||
if (throwable instanceof CompositeException) {
|
if (throwable instanceof CompositeException) {
|
||||||
for (Throwable element : ((CompositeException) throwable).getExceptions()) {
|
errors = ((CompositeException) throwable).getExceptions();
|
||||||
if (checkThrowable(element)) return;
|
} else {
|
||||||
|
errors = Collections.singletonList(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Throwable error : errors) {
|
||||||
|
if (isThrowableIgnored(error)) return;
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkThrowable(throwable)) return;
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(throwable);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||||
|
IOException.class, SocketException.class, // network api cancellation
|
||||||
|
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||||
|
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||||
|
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||||
|
IllegalStateException.class); // bug in operator
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportException(@NonNull final Throwable throwable) {
|
||||||
// Throw uncaught exception that will trigger the report system
|
// Throw uncaught exception that will trigger the report system
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
Thread.currentThread().getUncaughtExceptionHandler()
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
.uncaughtException(Thread.currentThread(), throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkThrowable(@NonNull Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
|
||||||
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
|
||||||
|
final int diskCacheSizeMb) {
|
||||||
|
return new ImageLoaderConfiguration.Builder(this)
|
||||||
|
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||||
|
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||||
|
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private void initACRA() {
|
private void initACRA() {
|
||||||
try {
|
try {
|
||||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||||
|
@ -152,4 +204,17 @@ public class App extends Application {
|
||||||
mNotificationManager.createNotificationChannel(mChannel);
|
mNotificationManager.createNotificationChannel(mChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static RefWatcher getRefWatcher(Context context) {
|
||||||
|
final App application = (App) context.getApplicationContext();
|
||||||
|
return application.refWatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RefWatcher installLeakCanary() {
|
||||||
|
return RefWatcher.DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isDisposedRxExceptionsReported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.AttrRes;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
import com.squareup.leakcanary.RefWatcher;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
|
|
||||||
|
@ -67,6 +64,14 @@ public abstract class BaseFragment extends Fragment {
|
||||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
RefWatcher refWatcher = App.getRefWatcher(getActivity());
|
||||||
|
if (refWatcher != null) refWatcher.watch(this);
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Init
|
// Init
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -78,33 +83,13 @@ public abstract class BaseFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// DisplayImageOptions default configurations
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static final DisplayImageOptions BASE_OPTIONS =
|
public void setTitle(String title) {
|
||||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||||
|
if (activity != null && activity.getSupportActionBar() != null) {
|
||||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
activity.getSupportActionBar().setTitle(title);
|
||||||
new DisplayImageOptions.Builder()
|
}
|
||||||
.cloneFrom(BASE_OPTIONS)
|
}
|
||||||
.showImageOnLoading(R.drawable.buddy)
|
|
||||||
.showImageForEmptyUri(R.drawable.buddy)
|
|
||||||
.showImageOnFail(R.drawable.buddy)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_OPTIONS)
|
|
||||||
.displayer(new FadeInBitmapDisplayer(250))
|
|
||||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
|
||||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_OPTIONS)
|
|
||||||
.showImageOnLoading(R.drawable.channel_banner)
|
|
||||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
|
||||||
.showImageOnFail(R.drawable.channel_banner)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStream;
|
||||||
import java.io.InterruptedIOException;
|
import java.util.Collections;
|
||||||
import java.net.URL;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -38,32 +39,38 @@ import javax.net.ssl.HttpsURLConnection;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
|
|
||||||
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
|
||||||
private static String mCookies = "";
|
|
||||||
|
|
||||||
private static Downloader instance = null;
|
private static Downloader instance;
|
||||||
|
private String mCookies;
|
||||||
|
private OkHttpClient client;
|
||||||
|
|
||||||
private Downloader() {
|
private Downloader(OkHttpClient.Builder builder) {
|
||||||
|
this.client = builder
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
//.cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), 16 * 1024 * 1024))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
|
*
|
||||||
|
* @param builder if null, default builder will be used
|
||||||
|
*/
|
||||||
|
public static Downloader init(@Nullable OkHttpClient.Builder builder) {
|
||||||
|
return instance = new Downloader(builder != null ? builder : new OkHttpClient.Builder());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Downloader getInstance() {
|
public static Downloader getInstance() {
|
||||||
if (instance == null) {
|
|
||||||
synchronized (Downloader.class) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new Downloader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void setCookies(String cookies) {
|
public String getCookies() {
|
||||||
Downloader.mCookies = cookies;
|
return mCookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized String getCookies() {
|
public void setCookies(String cookies) {
|
||||||
return Downloader.mCookies;
|
mCookies = cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,14 +99,44 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||||
URL url = new URL(siteUrl);
|
return getBody(siteUrl, customProperties).string();
|
||||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
|
||||||
Iterator it = customProperties.entrySet().iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Map.Entry pair = (Map.Entry) it.next();
|
|
||||||
con.setRequestProperty((String) pair.getKey(), (String) pair.getValue());
|
|
||||||
}
|
}
|
||||||
return dl(con);
|
|
||||||
|
public InputStream stream(String siteUrl) throws IOException {
|
||||||
|
try {
|
||||||
|
return getBody(siteUrl, Collections.emptyMap()).byteStream();
|
||||||
|
} catch (ReCaptchaException e) {
|
||||||
|
throw new IOException(e.getMessage(), e.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||||
|
final Request.Builder requestBuilder = new Request.Builder()
|
||||||
|
.method("GET", null).url(siteUrl)
|
||||||
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> header : customProperties.entrySet()) {
|
||||||
|
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(mCookies)) {
|
||||||
|
requestBuilder.addHeader("Cookie", mCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Request request = requestBuilder.build();
|
||||||
|
final Response response = client.newCall(request).execute();
|
||||||
|
final ResponseBody body = response.body();
|
||||||
|
|
||||||
|
if (response.code() == 429) {
|
||||||
|
throw new ReCaptchaException("reCaptcha Challenge requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body == null) {
|
||||||
|
response.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,57 +148,6 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
public String download(String siteUrl) throws IOException, ReCaptchaException {
|
||||||
URL url = new URL(siteUrl);
|
return download(siteUrl, Collections.emptyMap());
|
||||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
|
||||||
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
|
|
||||||
return dl(con);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common functionality between download(String url) and download(String url, String language)
|
|
||||||
*/
|
|
||||||
private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException {
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
BufferedReader in = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
con.setReadTimeout(30 * 1000);// 30s
|
|
||||||
con.setRequestMethod("GET");
|
|
||||||
con.setRequestProperty("User-Agent", USER_AGENT);
|
|
||||||
|
|
||||||
if (getCookies().length() > 0) {
|
|
||||||
con.setRequestProperty("Cookie", getCookies());
|
|
||||||
}
|
|
||||||
|
|
||||||
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
|
|
||||||
|
|
||||||
String inputLine;
|
|
||||||
while ((inputLine = in.readLine()) != null) {
|
|
||||||
response.append(inputLine);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("Downloader", "dl() ----- Exception thrown → " + e.getClass().getName());
|
|
||||||
|
|
||||||
if (ExtractorHelper.isInterruptedCaused(e)) {
|
|
||||||
throw new InterruptedIOException(e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* HTTP 429 == Too Many Request
|
|
||||||
* Receive from Youtube.com = ReCaptcha challenge request
|
|
||||||
* See : https://github.com/rg3/youtube-dl/issues/5138
|
|
||||||
*/
|
|
||||||
if (con.getResponseCode() == 429) {
|
|
||||||
throw new ReCaptchaException("reCaptcha Challenge requested");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IOException(con.getResponseCode() + " " + con.getResponseMessage(), e);
|
|
||||||
} finally {
|
|
||||||
if (in != null) {
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
46
app/src/main/java/org/schabi/newpipe/ImageDownloader.java
Normal file
46
app/src/main/java/org/schabi/newpipe/ImageDownloader.java
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class ImageDownloader extends BaseImageDownloader {
|
||||||
|
private final Resources resources;
|
||||||
|
private final SharedPreferences preferences;
|
||||||
|
private final String downloadThumbnailKey;
|
||||||
|
|
||||||
|
public ImageDownloader(Context context) {
|
||||||
|
super(context);
|
||||||
|
this.resources = context.getResources();
|
||||||
|
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDownloadingThumbnail() {
|
||||||
|
return preferences.getBoolean(downloadThumbnailKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
|
@Override
|
||||||
|
public InputStream getStream(String imageUri, Object extra) throws IOException {
|
||||||
|
if (isDownloadingThumbnail()) {
|
||||||
|
return super.getStream(imageUri, extra);
|
||||||
|
} else {
|
||||||
|
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
||||||
|
final Downloader downloader = (Downloader) NewPipe.getDownloader();
|
||||||
|
return downloader.stream(imageUri);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,12 +22,11 @@ package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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;
|
||||||
|
@ -37,46 +36,37 @@ import android.support.v7.app.ActionBarDrawerToggle;
|
||||||
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.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.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
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.report.ErrorActivity;
|
||||||
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;
|
||||||
|
private DrawerLayout drawer = null;
|
||||||
|
private NavigationView drawerItems = null;
|
||||||
|
private TextView headerServiceView = null;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Activity's LifeCycle
|
// Activity's LifeCycle
|
||||||
|
@ -86,7 +76,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,13 +87,12 @@ 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() {
|
||||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
drawer = findViewById(R.id.drawer_layout);
|
||||||
final NavigationView drawerItems = findViewById(R.id.navigation);
|
drawerItems = findViewById(R.id.navigation);
|
||||||
|
|
||||||
//drawerItems.setItemIconTintList(null); // Set null to use the original icon
|
//drawerItems.setItemIconTintList(null); // Set null to use the original icon
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||||
|
@ -129,18 +117,47 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
drawerItems.setNavigationItemSelectedListener(item -> {
|
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||||
|
|
||||||
|
setupDrawerFooter();
|
||||||
|
setupDrawerHeader();
|
||||||
|
} else {
|
||||||
|
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean changeService(MenuItem item) {
|
||||||
if (item.getGroupId() == R.id.menu_services_group) {
|
if (item.getGroupId() == R.id.menu_services_group) {
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||||
ServiceHelper.setSelectedServiceId(this, item.getTitle().toString());
|
ServiceHelper.setSelectedServiceId(this, item.getTitle().toString());
|
||||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||||
|
headerServiceView.setText("gurken");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
drawer.closeDrawers();
|
drawer.closeDrawers();
|
||||||
return true;
|
return true;
|
||||||
});
|
|
||||||
} else {
|
|
||||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupDrawerFooter() {
|
||||||
|
ImageButton settings = findViewById(R.id.drawer_settings);
|
||||||
|
ImageButton downloads = findViewById(R.id.drawer_downloads);
|
||||||
|
ImageButton history = findViewById(R.id.drawer_history);
|
||||||
|
|
||||||
|
settings.setOnClickListener(view -> NavigationHelper.openSettings(this));
|
||||||
|
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this));
|
||||||
|
history.setOnClickListener(view -> NavigationHelper.openHistory(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupDrawerHeader() {
|
||||||
|
headerServiceView = findViewById(R.id.drawer_header_service_view);
|
||||||
|
Button action = findViewById(R.id.drawer_header_action_button);
|
||||||
|
action.setOnClickListener(view -> {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse("https://newpipe.schabi.org/blog/"));
|
||||||
|
startActivity(intent);
|
||||||
|
drawer.closeDrawers();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -149,14 +166,25 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
|
||||||
if (!isChangingConfigurations()) {
|
if (!isChangingConfigurations()) {
|
||||||
StateSaver.clearStateFiles();
|
StateSaver.clearStateFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeHistory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
|
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
|
||||||
|
// when the user returns to MainActivity
|
||||||
|
drawer.closeDrawer(Gravity.START, false);
|
||||||
|
try {
|
||||||
|
if(BuildConfig.BUILD_TYPE != "release" ) {
|
||||||
|
String selectedServiceName = NewPipe.getService(
|
||||||
|
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||||
|
headerServiceView.setText(selectedServiceName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorActivity.reportUiError(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||||
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
||||||
|
@ -357,75 +385,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,25 +7,36 @@ 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 {
|
||||||
|
|
||||||
private static AppDatabase databaseInstance;
|
private static volatile AppDatabase databaseInstance;
|
||||||
|
|
||||||
private NewPipeDatabase() {
|
private NewPipeDatabase() {
|
||||||
//no instance
|
//no instance
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Context context) {
|
private static AppDatabase getDatabase(Context context) {
|
||||||
databaseInstance = Room.databaseBuilder(context.getApplicationContext(),
|
return Room
|
||||||
AppDatabase.class, DATABASE_NAME
|
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||||
).build();
|
.addMigrations(MIGRATION_11_12)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static AppDatabase getInstance() {
|
public static AppDatabase getInstance(@NonNull Context context) {
|
||||||
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
AppDatabase result = databaseInstance;
|
||||||
|
if (result == null) {
|
||||||
|
synchronized (NewPipeDatabase.class) {
|
||||||
|
result = databaseInstance;
|
||||||
|
if (result == null) {
|
||||||
|
databaseInstance = (result = getDatabase(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return databaseInstance;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||||
if (find_access_cookies(cookies)) {
|
if (find_access_cookies(cookies)) {
|
||||||
// Give cookies to Downloader class
|
// Give cookies to Downloader class
|
||||||
Downloader.setCookies(mCookies);
|
Downloader.getInstance().setCookies(mCookies);
|
||||||
|
|
||||||
// Closing activity and return to parent
|
// Closing activity and return to parent
|
||||||
setResult(RESULT_OK);
|
setResult(RESULT_OK);
|
||||||
|
|
|
@ -1,51 +1,78 @@
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.DrawableRes;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.Info;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
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.stream.StreamInfo;
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
|
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||||
|
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.PermissionHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
import icepick.Icepick;
|
import icepick.Icepick;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Observable;
|
import io.reactivex.Observable;
|
||||||
|
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.CompositeDisposable;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
/*
|
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
|
||||||
* RouterActivity.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Acitivty is designed to route share/open intents to the specified service, and
|
* Get the url from the intent and open it in the chosen preferred player
|
||||||
* to the part of the service which can handle the url.
|
|
||||||
*/
|
*/
|
||||||
public class RouterActivity extends AppCompatActivity {
|
public class RouterActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
|
protected int currentServiceId = -1;
|
||||||
|
private StreamingService currentService;
|
||||||
|
@State
|
||||||
|
protected LinkType currentLinkType;
|
||||||
|
@State
|
||||||
|
protected int selectedRadioPosition = -1;
|
||||||
|
protected int selectedPreviously = -1;
|
||||||
|
|
||||||
protected String currentUrl;
|
protected String currentUrl;
|
||||||
protected CompositeDisposable disposables = new CompositeDisposable();
|
protected CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
@ -62,6 +89,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||||
|
? R.style.RouterActivityThemeLight
|
||||||
|
: R.style.RouterActivityThemeDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -73,25 +104,43 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
handleUrl(currentUrl);
|
handleUrl(currentUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleUrl(String url) {
|
@Override
|
||||||
disposables.add(Observable
|
protected void onDestroy() {
|
||||||
.fromCallable(() -> NavigationHelper.getIntentByLink(this, url))
|
super.onDestroy();
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(intent -> {
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
|
|
||||||
finish();
|
disposables.clear();
|
||||||
}, this::handleError)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void handleError(Throwable error) {
|
private void handleUrl(String url) {
|
||||||
|
disposables.add(Observable
|
||||||
|
.fromCallable(() -> {
|
||||||
|
if (currentServiceId == -1) {
|
||||||
|
currentService = NewPipe.getServiceByUrl(url);
|
||||||
|
currentServiceId = currentService.getServiceId();
|
||||||
|
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||||
|
currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType);
|
||||||
|
} else {
|
||||||
|
currentService = NewPipe.getService(currentServiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentLinkType != LinkType.NONE;
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
if (result) {
|
||||||
|
onSuccess();
|
||||||
|
} else {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}, this::handleError));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleError(Throwable error) {
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
|
|
||||||
if (error instanceof ExtractionException) {
|
if (error instanceof ExtractionException) {
|
||||||
|
@ -103,11 +152,345 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void onError() {
|
||||||
protected void onDestroy() {
|
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||||
super.onDestroy();
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
disposables.clear();
|
protected void onSuccess() {
|
||||||
|
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||||
|
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||||
|
|
||||||
|
if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) {
|
||||||
|
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.)
|
||||||
|
if (currentService == ServiceList.SoundCloud) {
|
||||||
|
handleChoice(getString(R.string.background_player_key));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String playerChoiceKey = preferences.getString(
|
||||||
|
getString(R.string.preferred_open_action_key),
|
||||||
|
getString(R.string.preferred_open_action_default));
|
||||||
|
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||||
|
|
||||||
|
if (playerChoiceKey.equals(alwaysAskKey)) {
|
||||||
|
showDialog();
|
||||||
|
} else {
|
||||||
|
handleChoice(playerChoiceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDialog() {
|
||||||
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
|
||||||
|
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||||
|
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(themeWrapper);
|
||||||
|
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
||||||
|
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||||
|
|
||||||
|
final AdapterChoiceItem[] choices = {
|
||||||
|
new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||||
|
resolveResourceIdFromAttr(themeWrapper, R.attr.info)),
|
||||||
|
new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||||
|
resolveResourceIdFromAttr(themeWrapper, R.attr.play)),
|
||||||
|
new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||||
|
resolveResourceIdFromAttr(themeWrapper, R.attr.audio)),
|
||||||
|
new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||||
|
resolveResourceIdFromAttr(themeWrapper, R.attr.popup))
|
||||||
|
};
|
||||||
|
|
||||||
|
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||||
|
final int indexOfChild = radioGroup.indexOfChild(
|
||||||
|
radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
|
||||||
|
final AdapterChoiceItem choice = choices[indexOfChild];
|
||||||
|
|
||||||
|
handleChoice(choice.key);
|
||||||
|
|
||||||
|
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper)
|
||||||
|
.setTitle(R.string.preferred_player_share_menu_title)
|
||||||
|
.setView(radioGroup)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||||
|
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||||
|
.setOnDismissListener((dialog) -> finish())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
alertDialog.setOnShowListener(dialog -> {
|
||||||
|
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
|
||||||
|
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||||
|
final int indexOfChild = radioGroup.indexOfChild(v);
|
||||||
|
if (indexOfChild == -1) return;
|
||||||
|
|
||||||
|
selectedPreviously = selectedRadioPosition;
|
||||||
|
selectedRadioPosition = indexOfChild;
|
||||||
|
|
||||||
|
if (selectedPreviously == selectedRadioPosition) {
|
||||||
|
handleChoice(choices[selectedRadioPosition].key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int id = 12345;
|
||||||
|
for (AdapterChoiceItem item : choices) {
|
||||||
|
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||||
|
radioButton.setText(item.description);
|
||||||
|
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||||
|
radioButton.setChecked(false);
|
||||||
|
radioButton.setId(id++);
|
||||||
|
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||||
|
radioGroup.addView(radioButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedRadioPosition == -1) {
|
||||||
|
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
|
||||||
|
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||||
|
for (int i = 0; i < choices.length; i++) {
|
||||||
|
AdapterChoiceItem c = choices[i];
|
||||||
|
if (lastSelectedPlayer.equals(c.key)) {
|
||||||
|
selectedRadioPosition = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1);
|
||||||
|
if (selectedRadioPosition != -1) {
|
||||||
|
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
||||||
|
}
|
||||||
|
selectedPreviously = selectedRadioPosition;
|
||||||
|
|
||||||
|
alertDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
||||||
|
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||||
|
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||||
|
if (negativeButton == null || positiveButton == null) return;
|
||||||
|
|
||||||
|
negativeButton.setEnabled(state);
|
||||||
|
positiveButton.setEnabled(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleChoice(final String playerChoiceKey) {
|
||||||
|
if (Arrays.asList(getResources()
|
||||||
|
.getStringArray(R.array.preferred_open_action_values_list))
|
||||||
|
.contains(playerChoiceKey)) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
|
.putString(getString(R.string.preferred_open_action_last_selected_key),
|
||||||
|
playerChoiceKey).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerChoiceKey.equals(getString(R.string.popup_player_key))
|
||||||
|
&& !PermissionHelper.isPopupEnabled(this)) {
|
||||||
|
PermissionHelper.showPopupEnablementToast(this);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop and bypass FetcherService if InfoScreen was selected since
|
||||||
|
// StreamDetailFragment can fetch data itself
|
||||||
|
if(playerChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||||
|
disposables.add(Observable
|
||||||
|
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(intent -> {
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}, this::handleError)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Intent intent = new Intent(this, FetcherService.class);
|
||||||
|
intent.putExtra(FetcherService.KEY_CHOICE,
|
||||||
|
new Choice(currentService.getServiceId(),
|
||||||
|
currentLinkType,
|
||||||
|
currentUrl,
|
||||||
|
playerChoiceKey));
|
||||||
|
startService(intent);
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AdapterChoiceItem {
|
||||||
|
final String description, key;
|
||||||
|
@DrawableRes
|
||||||
|
final int icon;
|
||||||
|
|
||||||
|
AdapterChoiceItem(String key, String description, int icon) {
|
||||||
|
this.description = description;
|
||||||
|
this.key = key;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Choice implements Serializable {
|
||||||
|
final int serviceId;
|
||||||
|
final String url, playerChoice;
|
||||||
|
final LinkType linkType;
|
||||||
|
|
||||||
|
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
|
||||||
|
this.serviceId = serviceId;
|
||||||
|
this.linkType = linkType;
|
||||||
|
this.url = url;
|
||||||
|
this.playerChoice = playerChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Service Fetcher
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
public static class FetcherService extends IntentService {
|
||||||
|
|
||||||
|
private static final int ID = 456;
|
||||||
|
public static final String KEY_CHOICE = "key_choice";
|
||||||
|
private Disposable fetcher;
|
||||||
|
|
||||||
|
public FetcherService() {
|
||||||
|
super(FetcherService.class.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
startForeground(ID, createNotification().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(@Nullable Intent intent) {
|
||||||
|
if (intent == null) return;
|
||||||
|
|
||||||
|
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
||||||
|
if (!(serializable instanceof Choice)) return;
|
||||||
|
Choice playerChoice = (Choice) serializable;
|
||||||
|
handleChoice(playerChoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleChoice(Choice choice) {
|
||||||
|
Single<? extends Info> single = null;
|
||||||
|
UserAction userAction = UserAction.SOMETHING_ELSE;
|
||||||
|
|
||||||
|
switch (choice.linkType) {
|
||||||
|
case STREAM:
|
||||||
|
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_STREAM;
|
||||||
|
break;
|
||||||
|
case CHANNEL:
|
||||||
|
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_CHANNEL;
|
||||||
|
break;
|
||||||
|
case PLAYLIST:
|
||||||
|
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
|
||||||
|
userAction = UserAction.REQUESTED_PLAYLIST;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (single != null) {
|
||||||
|
final UserAction finalUserAction = userAction;
|
||||||
|
final Consumer<Info> resultHandler = getResultHandler(choice);
|
||||||
|
fetcher = single
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(info -> {
|
||||||
|
resultHandler.accept(info);
|
||||||
|
if (fetcher != null) fetcher.dispose();
|
||||||
|
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||||
|
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Consumer<Info> getResultHandler(Choice choice) {
|
||||||
|
return info -> {
|
||||||
|
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||||
|
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||||
|
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||||
|
|
||||||
|
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||||
|
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||||
|
boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this);
|
||||||
|
|
||||||
|
PlayQueue playQueue;
|
||||||
|
String playerChoice = choice.playerChoice;
|
||||||
|
|
||||||
|
if (info instanceof StreamInfo) {
|
||||||
|
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||||
|
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
||||||
|
|
||||||
|
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||||
|
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||||
|
|
||||||
|
} else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) {
|
||||||
|
NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||||
|
|
||||||
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
|
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||||
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
|
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||||
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
|
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
||||||
|
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||||
|
|
||||||
|
if (playerChoice.equals(videoPlayerKey)) {
|
||||||
|
NavigationHelper.playOnMainPlayer(this, playQueue);
|
||||||
|
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
||||||
|
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||||
|
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
stopForeground(true);
|
||||||
|
if (fetcher != null) fetcher.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationCompat.Builder createNotification() {
|
||||||
|
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
|
||||||
|
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -119,9 +502,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||||
* more details.
|
* more details.
|
||||||
*/
|
*/
|
||||||
protected final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||||
|
|
||||||
protected String getUrl(Intent intent) {
|
private String getUrl(Intent intent) {
|
||||||
// first gather data and find service
|
// first gather data and find service
|
||||||
String videoUrl = null;
|
String videoUrl = null;
|
||||||
if (intent.getData() != null) {
|
if (intent.getData() != null) {
|
||||||
|
@ -137,7 +520,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
return videoUrl;
|
return videoUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String removeHeadingGibberish(final String input) {
|
private String removeHeadingGibberish(final String input) {
|
||||||
int start = 0;
|
int start = 0;
|
||||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||||
|
@ -148,7 +531,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
return input.substring(start, input.length());
|
return input.substring(start, input.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String trim(final String input) {
|
private String trim(final String input) {
|
||||||
if (input == null || input.length() < 1) {
|
if (input == null || input.length() < 1) {
|
||||||
return input;
|
return input;
|
||||||
} else {
|
} else {
|
||||||
|
@ -188,5 +571,4 @@ public class RouterActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
return result.toArray(new String[result.size()]);
|
return result.toArray(new String[result.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,413 +0,0 @@
|
||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.IntentService;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.PersistableBundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.DrawableRes;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.ContextThemeWrapper;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.Info;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService;
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
|
||||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
|
||||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
|
||||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
|
||||||
import org.schabi.newpipe.report.UserAction;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import icepick.State;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
import io.reactivex.Single;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the url from the intent and open it in the chosen preferred player
|
|
||||||
*/
|
|
||||||
public class RouterPlayerActivity extends RouterActivity {
|
|
||||||
|
|
||||||
@State
|
|
||||||
protected int currentServiceId = -1;
|
|
||||||
private StreamingService currentService;
|
|
||||||
@State
|
|
||||||
protected LinkType currentLinkType;
|
|
||||||
@State
|
|
||||||
protected int selectedRadioPosition = -1;
|
|
||||||
protected int selectedPreviously = -1;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
|
|
||||||
super.onCreate(savedInstanceState, persistentState);
|
|
||||||
setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void handleUrl(String url) {
|
|
||||||
disposables.add(Observable
|
|
||||||
.fromCallable(() -> {
|
|
||||||
if (currentServiceId == -1) {
|
|
||||||
currentService = NewPipe.getServiceByUrl(url);
|
|
||||||
currentServiceId = currentService.getServiceId();
|
|
||||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
|
||||||
currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType);
|
|
||||||
} else {
|
|
||||||
currentService = NewPipe.getService(currentServiceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentLinkType != LinkType.NONE;
|
|
||||||
})
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(result -> {
|
|
||||||
if (result) {
|
|
||||||
onSuccess();
|
|
||||||
} else {
|
|
||||||
onError();
|
|
||||||
}
|
|
||||||
}, this::handleError));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onError() {
|
|
||||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onSuccess() {
|
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
|
||||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
|
||||||
|
|
||||||
if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) {
|
|
||||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.)
|
|
||||||
if (currentService == ServiceList.SoundCloud.getService()) {
|
|
||||||
handleChoice(getString(R.string.background_player_key));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String playerChoiceKey = preferences.getString(getString(R.string.preferred_player_key), getString(R.string.preferred_player_default));
|
|
||||||
final String alwaysAskKey = getString(R.string.always_ask_player_key);
|
|
||||||
|
|
||||||
if (playerChoiceKey.equals(alwaysAskKey)) {
|
|
||||||
showDialog();
|
|
||||||
} else {
|
|
||||||
handleChoice(playerChoiceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showDialog() {
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
|
|
||||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
|
||||||
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(themeWrapper);
|
|
||||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
|
||||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
|
||||||
|
|
||||||
final AdapterChoiceItem[] choices = {
|
|
||||||
new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
|
||||||
resolveResourceIdFromAttr(themeWrapper, R.attr.play)),
|
|
||||||
new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
|
||||||
resolveResourceIdFromAttr(themeWrapper, R.attr.audio)),
|
|
||||||
new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
|
||||||
resolveResourceIdFromAttr(themeWrapper, R.attr.popup))
|
|
||||||
};
|
|
||||||
|
|
||||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
|
||||||
final int indexOfChild = radioGroup.indexOfChild(radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
|
|
||||||
final AdapterChoiceItem choice = choices[indexOfChild];
|
|
||||||
|
|
||||||
handleChoice(choice.key);
|
|
||||||
|
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
preferences.edit().putString(getString(R.string.preferred_player_key), choice.key).apply();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper)
|
|
||||||
.setTitle(R.string.preferred_player_share_menu_title)
|
|
||||||
.setView(radioGroup)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
|
||||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
|
||||||
.setOnDismissListener((dialog) -> finish())
|
|
||||||
.create();
|
|
||||||
|
|
||||||
alertDialog.setOnShowListener(dialog -> {
|
|
||||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
|
||||||
});
|
|
||||||
|
|
||||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
|
|
||||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
|
||||||
final int indexOfChild = radioGroup.indexOfChild(v);
|
|
||||||
if (indexOfChild == -1) return;
|
|
||||||
|
|
||||||
selectedPreviously = selectedRadioPosition;
|
|
||||||
selectedRadioPosition = indexOfChild;
|
|
||||||
|
|
||||||
if (selectedPreviously == selectedRadioPosition) {
|
|
||||||
handleChoice(choices[selectedRadioPosition].key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int id = 12345;
|
|
||||||
for (AdapterChoiceItem item : choices) {
|
|
||||||
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
|
||||||
radioButton.setText(item.description);
|
|
||||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
|
|
||||||
radioButton.setChecked(false);
|
|
||||||
radioButton.setId(id++);
|
|
||||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
|
||||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
|
||||||
radioGroup.addView(radioButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedRadioPosition == -1) {
|
|
||||||
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_player_last_selected_key), null);
|
|
||||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
|
||||||
for (int i = 0; i < choices.length; i++) {
|
|
||||||
AdapterChoiceItem c = choices[i];
|
|
||||||
if (lastSelectedPlayer.equals(c.key)) {
|
|
||||||
selectedRadioPosition = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1);
|
|
||||||
if (selectedRadioPosition != -1) {
|
|
||||||
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
|
||||||
}
|
|
||||||
selectedPreviously = selectedRadioPosition;
|
|
||||||
|
|
||||||
alertDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
|
||||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
|
||||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
|
||||||
if (negativeButton == null || positiveButton == null) return;
|
|
||||||
|
|
||||||
negativeButton.setEnabled(state);
|
|
||||||
positiveButton.setEnabled(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleChoice(final String playerChoiceKey) {
|
|
||||||
if (Arrays.asList(getResources().getStringArray(R.array.preferred_player_values_list)).contains(playerChoiceKey)) {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
|
||||||
.putString(getString(R.string.preferred_player_last_selected_key), playerChoiceKey).apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
|
|
||||||
PermissionHelper.showPopupEnablementToast(this);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Intent intent = new Intent(this, FetcherService.class);
|
|
||||||
intent.putExtra(FetcherService.KEY_CHOICE, new Choice(currentService.getServiceId(), currentLinkType, currentUrl, playerChoiceKey));
|
|
||||||
startService(intent);
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AdapterChoiceItem {
|
|
||||||
final String description, key;
|
|
||||||
@DrawableRes
|
|
||||||
final int icon;
|
|
||||||
|
|
||||||
AdapterChoiceItem(String key, String description, int icon) {
|
|
||||||
this.description = description;
|
|
||||||
this.key = key;
|
|
||||||
this.icon = icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Choice implements Serializable {
|
|
||||||
final int serviceId;
|
|
||||||
final String url, playerChoice;
|
|
||||||
final LinkType linkType;
|
|
||||||
|
|
||||||
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.linkType = linkType;
|
|
||||||
this.url = url;
|
|
||||||
this.playerChoice = playerChoice;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return serviceId + ":" + url + " > " + linkType + " ::: " + playerChoice;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Service Fetcher
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
public static class FetcherService extends IntentService {
|
|
||||||
|
|
||||||
private static final int ID = 456;
|
|
||||||
public static final String KEY_CHOICE = "key_choice";
|
|
||||||
private Disposable fetcher;
|
|
||||||
|
|
||||||
public FetcherService() {
|
|
||||||
super(FetcherService.class.getSimpleName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
startForeground(ID, createNotification().build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onHandleIntent(@Nullable Intent intent) {
|
|
||||||
if (intent == null) return;
|
|
||||||
|
|
||||||
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
|
||||||
if (!(serializable instanceof Choice)) return;
|
|
||||||
Choice playerChoice = (Choice) serializable;
|
|
||||||
handleChoice(playerChoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleChoice(Choice choice) {
|
|
||||||
Single<? extends Info> single = null;
|
|
||||||
UserAction userAction = UserAction.SOMETHING_ELSE;
|
|
||||||
|
|
||||||
switch (choice.linkType) {
|
|
||||||
case STREAM:
|
|
||||||
single = ExtractorHelper.getStreamInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_STREAM;
|
|
||||||
break;
|
|
||||||
case CHANNEL:
|
|
||||||
single = ExtractorHelper.getChannelInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_CHANNEL;
|
|
||||||
break;
|
|
||||||
case PLAYLIST:
|
|
||||||
single = ExtractorHelper.getPlaylistInfo(choice.serviceId, choice.url, false);
|
|
||||||
userAction = UserAction.REQUESTED_PLAYLIST;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (single != null) {
|
|
||||||
final UserAction finalUserAction = userAction;
|
|
||||||
final Consumer<Info> resultHandler = getResultHandler(choice);
|
|
||||||
fetcher = single
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(info -> {
|
|
||||||
resultHandler.accept(info);
|
|
||||||
if (fetcher != null) fetcher.dispose();
|
|
||||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
|
||||||
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Consumer<Info> getResultHandler(Choice choice) {
|
|
||||||
return info -> {
|
|
||||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
|
||||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
|
||||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
|
||||||
|
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
|
||||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
|
||||||
boolean useOldVideoPlayer = PlayerHelper.isUsingOldPlayer(this);
|
|
||||||
|
|
||||||
PlayQueue playQueue;
|
|
||||||
String playerChoice = choice.playerChoice;
|
|
||||||
|
|
||||||
if (info instanceof StreamInfo) {
|
|
||||||
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
|
||||||
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
|
||||||
|
|
||||||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
|
||||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
|
||||||
|
|
||||||
} else if (playerChoice.equals(videoPlayerKey) && useOldVideoPlayer) {
|
|
||||||
NavigationHelper.playOnOldVideoPlayer(this, (StreamInfo) info);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
|
||||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
|
||||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
|
||||||
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
|
||||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
|
||||||
|
|
||||||
if (playerChoice.equals(videoPlayerKey)) {
|
|
||||||
NavigationHelper.playOnMainPlayer(this, playQueue);
|
|
||||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue);
|
|
||||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
|
||||||
NavigationHelper.playOnPopupPlayer(this, playQueue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
stopForeground(true);
|
|
||||||
if (fetcher != null) fetcher.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private NotificationCompat.Builder createNotification() {
|
|
||||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
|
|
||||||
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
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.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
|
||||||
|
item.getUploaderName(), item.getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public StreamEntity(final StreamInfo info) {
|
||||||
|
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
|
||||||
|
info.getUploaderName(), info.getDuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package org.schabi.newpipe.database.subscription;
|
package org.schabi.newpipe.database.subscription;
|
||||||
|
|
||||||
import android.arch.persistence.room.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.Query;
|
||||||
|
import android.arch.persistence.room.Transaction;
|
||||||
|
|
||||||
import org.schabi.newpipe.database.BasicDAO;
|
import org.schabi.newpipe.database.BasicDAO;
|
||||||
|
|
||||||
|
@ -11,24 +14,56 @@ import io.reactivex.Flowable;
|
||||||
|
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||||
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID;
|
||||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
|
public abstract class SubscriptionDAO implements BasicDAO<SubscriptionEntity> {
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
||||||
Flowable<List<SubscriptionEntity>> getAll();
|
public abstract Flowable<List<SubscriptionEntity>> getAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
||||||
int deleteAll();
|
public abstract int deleteAll();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
public abstract Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
||||||
|
|
||||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||||
Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
public abstract Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
||||||
|
|
||||||
|
@Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||||
|
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||||
|
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||||
|
abstract Long getSubscriptionIdInternal(int serviceId, String url);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract Long insertInternal(final SubscriptionEntity entities);
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public List<SubscriptionEntity> upsertAll(List<SubscriptionEntity> entities) {
|
||||||
|
for (SubscriptionEntity entity : entities) {
|
||||||
|
Long uid = insertInternal(entity);
|
||||||
|
|
||||||
|
if (uid != -1) {
|
||||||
|
entity.setUid(uid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl());
|
||||||
|
entity.setUid(uid);
|
||||||
|
|
||||||
|
if (uid == -1) {
|
||||||
|
throw new IllegalStateException("Invalid subscription id (-1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ 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.Index;
|
||||||
import android.arch.persistence.room.PrimaryKey;
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
|
||||||
|
@ -17,6 +19,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
|
||||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||||
public class SubscriptionEntity {
|
public class SubscriptionEntity {
|
||||||
|
|
||||||
|
final static String SUBSCRIPTION_UID = "uid";
|
||||||
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
||||||
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||||
final static String SUBSCRIPTION_URL = "url";
|
final static String SUBSCRIPTION_URL = "url";
|
||||||
|
@ -50,8 +53,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,9 +119,18 @@ public class SubscriptionEntity {
|
||||||
@Ignore
|
@Ignore
|
||||||
public ChannelInfoItem toChannelInfoItem() {
|
public ChannelInfoItem toChannelInfoItem() {
|
||||||
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||||
item.thumbnail_url = getAvatarUrl();
|
item.setThumbnailUrl(getAvatarUrl());
|
||||||
item.subscriber_count = getSubscriberCount();
|
item.setSubscriberCount(getSubscriberCount());
|
||||||
item.description = getDescription();
|
item.setDescription(getDescription());
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
public static SubscriptionEntity from(@NonNull ChannelInfo info) {
|
||||||
|
SubscriptionEntity result = new SubscriptionEntity();
|
||||||
|
result.setServiceId(info.getServiceId());
|
||||||
|
result.setUrl(info.getUrl());
|
||||||
|
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,7 +205,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||||
switch (checkedId) {
|
switch (checkedId) {
|
||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
setupAudioSpinner(currentInfo.audio_streams, streamsSpinner);
|
setupAudioSpinner(currentInfo.getAudioStreams(), streamsSpinner);
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
||||||
|
|
|
@ -246,13 +246,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
|
||||||
if (activity != null && activity.getSupportActionBar() != null) {
|
|
||||||
activity.getSupportActionBar().setTitle(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void openUrlInBrowser(String url) {
|
protected void openUrlInBrowser(String url) {
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -46,7 +47,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
// Constants
|
// Constants
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getId();
|
private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId();
|
||||||
private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ";
|
private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ";
|
||||||
private static final String FALLBACK_CHANNEL_NAME = "Music";
|
private static final String FALLBACK_CHANNEL_NAME = "Music";
|
||||||
private static final String FALLBACK_KIOSK_ID = "Trending";
|
private static final String FALLBACK_KIOSK_ID = "Trending";
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +106,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, 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 + "]");
|
||||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||||
SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk));
|
SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk));
|
||||||
try {
|
try {
|
||||||
createKioskMenu(kioskMenu, inflater);
|
createKioskMenu(kioskMenu, inflater);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -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());
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 18.08.15.
|
|
||||||
* <p>
|
|
||||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
|
||||||
* DetailsMenuHandler.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
|
||||||
class ActionBarHandler {
|
|
||||||
private static final String TAG = "ActionBarHandler";
|
|
||||||
|
|
||||||
private AppCompatActivity activity;
|
|
||||||
private int selectedVideoStream = -1;
|
|
||||||
|
|
||||||
private SharedPreferences defaultPreferences;
|
|
||||||
|
|
||||||
private Menu menu;
|
|
||||||
|
|
||||||
// Only callbacks are listed here, there are more actions which don't need a callback.
|
|
||||||
// those are edited directly. Typically VideoDetailFragment will implement those callbacks.
|
|
||||||
private OnActionListener onShareListener;
|
|
||||||
private OnActionListener onOpenInBrowserListener;
|
|
||||||
private OnActionListener onDownloadListener;
|
|
||||||
private OnActionListener onPlayWithKodiListener;
|
|
||||||
|
|
||||||
// Triggered when a stream related action is triggered.
|
|
||||||
public interface OnActionListener {
|
|
||||||
void onActionSelected(int selectedStreamId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActionBarHandler(AppCompatActivity activity) {
|
|
||||||
this.activity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setupStreamList(final List<VideoStream> videoStreams, Spinner toolbarSpinner) {
|
|
||||||
if (activity == null) return;
|
|
||||||
|
|
||||||
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, videoStreams);
|
|
||||||
|
|
||||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
|
||||||
toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled));
|
|
||||||
toolbarSpinner.setSelection(selectedVideoStream);
|
|
||||||
toolbarSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
selectedVideoStream = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setupMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
this.menu = menu;
|
|
||||||
|
|
||||||
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
|
||||||
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
|
||||||
|
|
||||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
|
||||||
inflater.inflate(R.menu.video_detail_menu, menu);
|
|
||||||
|
|
||||||
updateItemsVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateItemsVisibility(){
|
|
||||||
showPlayWithKodiAction(defaultPreferences.getBoolean(activity.getString(R.string.show_play_with_kodi_key), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onItemSelected(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
switch (id) {
|
|
||||||
case R.id.menu_item_share: {
|
|
||||||
if (onShareListener != null) {
|
|
||||||
onShareListener.onActionSelected(selectedVideoStream);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.menu_item_openInBrowser: {
|
|
||||||
if (onOpenInBrowserListener != null) {
|
|
||||||
onOpenInBrowserListener.onActionSelected(selectedVideoStream);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.menu_item_download:
|
|
||||||
if (onDownloadListener != null) {
|
|
||||||
onDownloadListener.onActionSelected(selectedVideoStream);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case R.id.action_play_with_kodi:
|
|
||||||
if (onPlayWithKodiListener != null) {
|
|
||||||
onPlayWithKodiListener.onActionSelected(selectedVideoStream);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
Log.e(TAG, "Menu Item not known");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedVideoStream() {
|
|
||||||
return selectedVideoStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnShareListener(OnActionListener listener) {
|
|
||||||
onShareListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnOpenInBrowserListener(OnActionListener listener) {
|
|
||||||
onOpenInBrowserListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnDownloadListener(OnActionListener listener) {
|
|
||||||
onDownloadListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPlayWithKodiListener(OnActionListener listener) {
|
|
||||||
onPlayWithKodiListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showDownloadAction(boolean visible) {
|
|
||||||
menu.findItem(R.id.menu_item_download).setVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showPlayWithKodiAction(boolean visible) {
|
|
||||||
menu.findItem(R.id.action_play_with_kodi).setVisible(visible);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,6 +31,7 @@ import android.view.MenuItem;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
@ -42,6 +43,7 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
|
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||||
|
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -55,10 +57,11 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
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.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;
|
||||||
|
@ -71,10 +74,12 @@ import org.schabi.newpipe.report.ErrorActivity;
|
||||||
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.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.InfoCache;
|
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;
|
||||||
|
|
||||||
|
@ -88,19 +93,21 @@ 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.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.Consumer;
|
|
||||||
import io.reactivex.functions.Function;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implements BackPressable, SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener, View.OnLongClickListener {
|
public class VideoDetailFragment
|
||||||
|
extends BaseStateFragment<StreamInfo>
|
||||||
|
implements BackPressable,
|
||||||
|
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||||
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener {
|
||||||
public static final String AUTO_PLAY = "auto_play";
|
public static final String AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
// Amount of videos to show on start
|
// Amount of videos to show on start
|
||||||
private static final int INITIAL_RELATED_VIDEOS = 8;
|
private static final int INITIAL_RELATED_VIDEOS = 8;
|
||||||
|
|
||||||
private ActionBarHandler actionBarHandler;
|
|
||||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
private ArrayList<VideoStream> sortedStreamVideosList;
|
||||||
|
|
||||||
private InfoItemBuilder infoItemBuilder = null;
|
private InfoItemBuilder infoItemBuilder = null;
|
||||||
|
@ -125,10 +132,14 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private Disposable currentWorker;
|
private Disposable currentWorker;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
|
private int selectedVideoStream = -1;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private Menu menu;
|
||||||
|
|
||||||
private Spinner spinnerToolbar;
|
private Spinner spinnerToolbar;
|
||||||
|
|
||||||
private ParallaxScrollView parallaxScrollRootView;
|
private ParallaxScrollView parallaxScrollRootView;
|
||||||
|
@ -145,7 +156,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
private TextView detailControlsBackground;
|
private TextView detailControlsBackground;
|
||||||
private TextView detailControlsPopup;
|
private TextView detailControlsPopup;
|
||||||
|
private TextView detailControlsAddToPlaylist;
|
||||||
|
private TextView detailControlsDownload;
|
||||||
private TextView appendControlsDetail;
|
private TextView appendControlsDetail;
|
||||||
|
private TextView detailDurationView;
|
||||||
|
|
||||||
private LinearLayout videoDescriptionRootLayout;
|
private LinearLayout videoDescriptionRootLayout;
|
||||||
private TextView videoUploadDateView;
|
private TextView videoUploadDateView;
|
||||||
|
@ -166,6 +180,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private LinearLayout relatedStreamsView;
|
private LinearLayout relatedStreamsView;
|
||||||
private ImageButton relatedStreamExpandButton;
|
private ImageButton relatedStreamExpandButton;
|
||||||
|
|
||||||
|
|
||||||
/*////////////////////////////////////////////////////////////////////////*/
|
/*////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) {
|
public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String name) {
|
||||||
|
@ -183,8 +198,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_next_video_key), true);
|
showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity).registerOnSharedPreferenceChangeListener(this);
|
.getBoolean(getString(R.string.show_next_video_key), true);
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
.registerOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -205,10 +222,13 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (updateFlags != 0) {
|
if (updateFlags != 0) {
|
||||||
if (!isLoading.get() && currentInfo != null) {
|
if (!isLoading.get() && currentInfo != null) {
|
||||||
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo);
|
if ((updateFlags & RELATED_STREAMS_UPDATE_FLAG) != 0) initRelatedVideos(currentInfo);
|
||||||
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBarHandler(currentInfo);
|
if ((updateFlags & RESOLUTIONS_MENU_UPDATE_FLAG) != 0) setupActionBar(currentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0 && actionBarHandler != null) actionBarHandler.updateItemsVisibility();
|
if ((updateFlags & TOOLBAR_ITEMS_UPDATE_FLAG) != 0
|
||||||
|
&& menu != null) {
|
||||||
|
updateMenuItemVisibility();
|
||||||
|
}
|
||||||
updateFlags = 0;
|
updateFlags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +241,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
PreferenceManager.getDefaultSharedPreferences(activity).unregisterOnSharedPreferenceChangeListener(this);
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
|
|
||||||
if (currentWorker != null) currentWorker.dispose();
|
if (currentWorker != null) currentWorker.dispose();
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) disposables.clear();
|
||||||
|
@ -282,7 +303,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
// Check if the next video label and video is visible,
|
// Check if the next video label and video is visible,
|
||||||
// if it is, include the two elements in the next check
|
// if it is, include the two elements in the next check
|
||||||
int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0;
|
int nextCount = currentInfo != null && currentInfo.getNextVideo() != null ? 2 : 0;
|
||||||
if (relatedStreamsView != null && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) {
|
if (relatedStreamsView != null
|
||||||
|
&& relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) {
|
||||||
outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true);
|
outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +324,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (serializable instanceof StreamInfo) {
|
if (serializable instanceof StreamInfo) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
currentInfo = (StreamInfo) serializable;
|
currentInfo = (StreamInfo) serializable;
|
||||||
InfoCache.getInstance().putInfo(currentInfo);
|
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
serializable = savedState.getSerializable(STACK_KEY);
|
serializable = savedState.getSerializable(STACK_KEY);
|
||||||
|
@ -327,6 +349,30 @@ 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_controls_download:
|
||||||
|
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DownloadDialog downloadDialog =
|
||||||
|
DownloadDialog.newInstance(currentInfo,
|
||||||
|
sortedStreamVideosList,
|
||||||
|
selectedVideoStream);
|
||||||
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Toast.makeText(activity,
|
||||||
|
R.string.could_not_setup_download_menu,
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
@ -339,7 +385,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_thumbnail_root_layout:
|
case R.id.detail_thumbnail_root_layout:
|
||||||
if (currentInfo.video_streams.isEmpty() && currentInfo.video_only_streams.isEmpty()) {
|
if (currentInfo.getVideoStreams().isEmpty()
|
||||||
|
&& currentInfo.getVideoOnlyStreams().isEmpty()) {
|
||||||
openBackgroundPlayer(false);
|
openBackgroundPlayer(false);
|
||||||
} else {
|
} else {
|
||||||
openVideoPlayer();
|
openVideoPlayer();
|
||||||
|
@ -390,8 +437,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
int initialCount = INITIAL_RELATED_VIDEOS + nextCount;
|
int initialCount = INITIAL_RELATED_VIDEOS + nextCount;
|
||||||
|
|
||||||
if (relatedStreamsView.getChildCount() > initialCount) {
|
if (relatedStreamsView.getChildCount() > initialCount) {
|
||||||
relatedStreamsView.removeViews(initialCount, relatedStreamsView.getChildCount() - (initialCount));
|
relatedStreamsView.removeViews(initialCount,
|
||||||
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
relatedStreamsView.getChildCount() - (initialCount));
|
||||||
|
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
|
||||||
|
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,7 +450,9 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
//Log.d(TAG, "i = " + i);
|
//Log.d(TAG, "i = " + i);
|
||||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
|
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item));
|
||||||
}
|
}
|
||||||
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse)));
|
relatedStreamExpandButton.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(activity,
|
||||||
|
ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.collapse)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -411,7 +462,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
super.initViews(rootView, savedInstanceState);
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
||||||
|
|
||||||
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
||||||
|
@ -429,7 +479,10 @@ 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);
|
||||||
|
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
|
||||||
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
||||||
|
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
||||||
|
|
||||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||||
|
@ -454,7 +507,6 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand);
|
relatedStreamExpandButton = rootView.findViewById(R.id.detail_related_streams_expand);
|
||||||
|
|
||||||
actionBarHandler = new ActionBarHandler(activity);
|
|
||||||
infoItemBuilder = new InfoItemBuilder(activity);
|
infoItemBuilder = new InfoItemBuilder(activity);
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
}
|
}
|
||||||
|
@ -462,7 +514,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 +531,8 @@ 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);
|
||||||
|
detailControlsDownload.setOnClickListener(this);
|
||||||
relatedStreamExpandButton.setOnClickListener(this);
|
relatedStreamExpandButton.setOnClickListener(this);
|
||||||
|
|
||||||
detailControlsBackground.setLongClickable(true);
|
detailControlsBackground.setLongClickable(true);
|
||||||
|
@ -498,9 +552,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
context.getResources().getString(R.string.enqueue_on_popup)
|
context.getResources().getString(R.string.enqueue_on_popup)
|
||||||
};
|
};
|
||||||
|
|
||||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int 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));
|
||||||
|
@ -511,44 +563,45 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private View.OnTouchListener getOnControlsTouchListener() {
|
private View.OnTouchListener getOnControlsTouchListener() {
|
||||||
return new View.OnTouchListener() {
|
return (View view, MotionEvent motionEvent) -> {
|
||||||
@Override
|
if (!PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
|
||||||
if (!PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_hold_to_append_key), true)) return false;
|
|
||||||
|
|
||||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
|
||||||
animateView(appendControlsDetail, true, 250, 0, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
animateView(appendControlsDetail, false, 1500, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
animateView(appendControlsDetail, true, 250, 0, () ->
|
||||||
|
animateView(appendControlsDetail, false, 1500, 1000));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initThumbnailViews(StreamInfo info) {
|
private void initThumbnailViews(@NonNull StreamInfo info) {
|
||||||
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||||
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
||||||
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView, DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() {
|
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
|
||||||
|
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||||
ErrorActivity.reportError(activity, failReason.getCause(), null, activity.findViewById(android.R.id.content), ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE, NewPipe.getNameOfService(currentInfo.getServiceId()), imageUri, R.string.could_not_load_thumbnails));
|
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||||
|
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
|
||||||
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
||||||
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS);
|
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
|
||||||
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,14 +610,18 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
if (info.getNextVideo() != null && showRelatedStreams) {
|
if (info.getNextVideo() != null && showRelatedStreams) {
|
||||||
nextStreamTitle.setVisibility(View.VISIBLE);
|
nextStreamTitle.setVisibility(View.VISIBLE);
|
||||||
relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo()));
|
relatedStreamsView.addView(
|
||||||
|
infoItemBuilder.buildView(relatedStreamsView, info.getNextVideo()));
|
||||||
relatedStreamsView.addView(getSeparatorView());
|
relatedStreamsView.addView(getSeparatorView());
|
||||||
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
||||||
} else nextStreamTitle.setVisibility(View.GONE);
|
} else nextStreamTitle.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) {
|
if (info.getRelatedStreams() != null
|
||||||
|
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
|
||||||
//long first = System.nanoTime(), each;
|
//long first = System.nanoTime(), each;
|
||||||
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS ? INITIAL_RELATED_VIDEOS : info.getRelatedStreams().size();
|
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS
|
||||||
|
? INITIAL_RELATED_VIDEOS
|
||||||
|
: info.getRelatedStreams().size();
|
||||||
for (int i = 0; i < to; i++) {
|
for (int i = 0; i < to; i++) {
|
||||||
InfoItem item = info.getRelatedStreams().get(i);
|
InfoItem item = info.getRelatedStreams().get(i);
|
||||||
//each = System.nanoTime();
|
//each = System.nanoTime();
|
||||||
|
@ -576,7 +633,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
||||||
relatedStreamExpandButton.setVisibility(View.VISIBLE);
|
relatedStreamExpandButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(
|
||||||
|
activity, ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.expand)));
|
||||||
} else {
|
} else {
|
||||||
if (info.getNextVideo() == null) relatedStreamRootLayout.setVisibility(View.GONE);
|
if (info.getNextVideo() == null) relatedStreamRootLayout.setVisibility(View.GONE);
|
||||||
relatedStreamExpandButton.setVisibility(View.GONE);
|
relatedStreamExpandButton.setVisibility(View.GONE);
|
||||||
|
@ -589,7 +647,15 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
actionBarHandler.setupMenu(menu, inflater);
|
this.menu = menu;
|
||||||
|
|
||||||
|
// CAUTION set item properties programmatically otherwise it would not be accepted by
|
||||||
|
// appcompat itemsinflater.inflate(R.menu.videoitem_detail, menu);
|
||||||
|
|
||||||
|
inflater.inflate(R.menu.video_detail_menu, menu);
|
||||||
|
|
||||||
|
updateMenuItemVisibility();
|
||||||
|
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if (supportActionBar != null) {
|
if (supportActionBar != null) {
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
@ -597,73 +663,86 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateMenuItemVisibility() {
|
||||||
|
|
||||||
|
// show kodi if set in settings
|
||||||
|
menu.findItem(R.id.action_play_with_kodi).setVisible(
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(
|
||||||
|
activity.getString(R.string.show_play_with_kodi_key), false));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
return (!isLoading.get() && actionBarHandler.onItemSelected(item)) || super.onOptionsItemSelected(item);
|
if(isLoading.get()) {
|
||||||
|
// if is still loading block menu
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = item.getItemId();
|
||||||
|
switch (id) {
|
||||||
|
case R.id.menu_item_share: {
|
||||||
|
if(currentInfo != null) {
|
||||||
|
shareUrl(currentInfo.getName(), url);
|
||||||
|
} else {
|
||||||
|
shareUrl(url, url);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case R.id.menu_item_openInBrowser: {
|
||||||
|
openUrlInBrowser(url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case R.id.action_play_with_kodi:
|
||||||
|
try {
|
||||||
|
NavigationHelper.playWithKore(activity, Uri.parse(
|
||||||
|
url.replace("https", "http")));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
|
||||||
|
showInstallKoreDialog(activity);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void showInstallKoreDialog(final Context context) {
|
private static void showInstallKoreDialog(final Context context) {
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||||
builder.setMessage(R.string.kore_not_found)
|
builder.setMessage(R.string.kore_not_found)
|
||||||
.setPositiveButton(R.string.install, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
|
||||||
@Override
|
NavigationHelper.installKore(context))
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {});
|
||||||
NavigationHelper.installKore(context);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.create().show();
|
builder.create().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupActionBarHandler(final StreamInfo info) {
|
private void setupActionBarOnError(final String url) {
|
||||||
|
if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]");
|
||||||
|
Log.e("-----", "missing code");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupActionBar(final StreamInfo info) {
|
||||||
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
||||||
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(
|
||||||
actionBarHandler.setupStreamList(sortedStreamVideosList, spinnerToolbar);
|
activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
||||||
actionBarHandler.setOnShareListener(selectedStreamId -> shareUrl(info.name, info.url));
|
|
||||||
|
|
||||||
actionBarHandler.setOnOpenInBrowserListener(new ActionBarHandler.OnActionListener() {
|
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList);
|
||||||
|
|
||||||
|
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
|
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||||
|
spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList,
|
||||||
|
isExternalPlayerEnabled));
|
||||||
|
spinnerToolbar.setSelection(selectedVideoStream);
|
||||||
|
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onActionSelected(int selectedStreamId) {
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
openUrlInBrowser(info.getUrl());
|
selectedVideoStream = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
|
||||||
@Override
|
|
||||||
public void onActionSelected(int selectedStreamId) {
|
|
||||||
try {
|
|
||||||
NavigationHelper.playWithKore(activity, Uri.parse(info.getUrl().replace("https", "http")));
|
|
||||||
if(activity instanceof HistoryListener) {
|
|
||||||
((HistoryListener) activity).onVideoPlayed(info, null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
if(DEBUG) Log.i(TAG, "Failed to start kore", e);
|
|
||||||
showInstallKoreDialog(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
|
|
||||||
@Override
|
|
||||||
public void onActionSelected(int selectedStreamId) {
|
|
||||||
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(info, sortedStreamVideosList, selectedStreamId);
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(activity, R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show();
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -698,7 +777,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
public void setTitleToUrl(int serviceId, String videoUrl, String name) {
|
public void setTitleToUrl(int serviceId, String videoUrl, String name) {
|
||||||
if (name != null && !name.isEmpty()) {
|
if (name != null && !name.isEmpty()) {
|
||||||
for (StackItem stackItem : stack) {
|
for (StackItem stackItem : stack) {
|
||||||
if (stack.peek().getServiceId() == serviceId && stackItem.getUrl().equals(videoUrl)) stackItem.setTitle(name);
|
if (stack.peek().getServiceId() == serviceId
|
||||||
|
&& stackItem.getUrl().equals(videoUrl)) {
|
||||||
|
stackItem.setTitle(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -740,17 +822,17 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
pushToStack(serviceId, url, name);
|
pushToStack(serviceId, url, name);
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): " + parallaxScrollRootView.getScrollY());
|
Log.d(TAG, "prepareAndHandleInfo() called parallaxScrollRootView.getScrollY(): "
|
||||||
|
+ parallaxScrollRootView.getScrollY());
|
||||||
final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int)
|
final boolean greaterThanThreshold = parallaxScrollRootView.getScrollY() > (int)
|
||||||
(getResources().getDisplayMetrics().heightPixels * .1f);
|
(getResources().getDisplayMetrics().heightPixels * .1f);
|
||||||
|
|
||||||
if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0);
|
if (scrollToTop) parallaxScrollRootView.smoothScrollTo(0, 0);
|
||||||
animateView(contentRootLayoutHiding, false, greaterThanThreshold ? 250 : 0, 0, new Runnable() {
|
animateView(contentRootLayoutHiding,
|
||||||
@Override
|
false,
|
||||||
public void run() {
|
greaterThanThreshold ? 250 : 0, 0, () -> {
|
||||||
handleResult(info);
|
handleResult(info);
|
||||||
showContentWithAnimation(120, 0, .01f);
|
showContentWithAnimation(120, 0, .01f);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -770,20 +852,14 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<StreamInfo>() {
|
.subscribe((@NonNull StreamInfo result) -> {
|
||||||
@Override
|
|
||||||
public void accept(@NonNull StreamInfo result) throws Exception {
|
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
showContentWithAnimation(120, 0, 0);
|
showContentWithAnimation(120, 0, 0);
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
}
|
}, (@NonNull Throwable throwable) -> {
|
||||||
}, new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
onError(throwable);
|
onError(throwable);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -792,11 +868,8 @@ 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);
|
||||||
|
@ -804,7 +877,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
|
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
|
||||||
openNormalBackgroundPlayer(append);
|
openNormalBackgroundPlayer(append);
|
||||||
} else {
|
} else {
|
||||||
NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), audioStream);
|
NavigationHelper.playOnExternalPlayer(activity,
|
||||||
|
currentInfo.getName(),
|
||||||
|
currentInfo.getUploaderName(),
|
||||||
|
audioStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,10 +890,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,12 +905,12 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private void openVideoPlayer() {
|
private void openVideoPlayer() {
|
||||||
VideoStream selectedVideoStream = getSelectedVideoStream();
|
VideoStream selectedVideoStream = getSelectedVideoStream();
|
||||||
|
|
||||||
if (activity instanceof HistoryListener) {
|
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
((HistoryListener) activity).onVideoPlayed(currentInfo, selectedVideoStream);
|
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||||
}
|
NavigationHelper.playOnExternalPlayer(activity,
|
||||||
|
currentInfo.getName(),
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
currentInfo.getUploaderName(),
|
||||||
NavigationHelper.playOnExternalPlayer(activity, currentInfo.getName(), currentInfo.getUploaderName(), selectedVideoStream);
|
selectedVideoStream);
|
||||||
} else {
|
} else {
|
||||||
openNormalPlayer(selectedVideoStream);
|
openNormalPlayer(selectedVideoStream);
|
||||||
}
|
}
|
||||||
|
@ -859,7 +931,10 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (!useOldPlayer) {
|
if (!useOldPlayer) {
|
||||||
// ExoPlayer
|
// ExoPlayer
|
||||||
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
|
final PlayQueue playQueue = new SinglePlayQueue(currentInfo);
|
||||||
mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, getSelectedVideoStream().getResolution());
|
mIntent = NavigationHelper.getPlayerIntent(activity,
|
||||||
|
MainVideoPlayer.class,
|
||||||
|
playQueue,
|
||||||
|
getSelectedVideoStream().getResolution());
|
||||||
} else {
|
} else {
|
||||||
// Internal Player
|
// Internal Player
|
||||||
mIntent = new Intent(activity, PlayVideoActivity.class)
|
mIntent = new Intent(activity, PlayVideoActivity.class)
|
||||||
|
@ -880,7 +955,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
}
|
}
|
||||||
|
|
||||||
private VideoStream getSelectedVideoStream() {
|
private VideoStream getSelectedVideoStream() {
|
||||||
return sortedStreamVideosList.get(actionBarHandler.getSelectedVideoStream());
|
return sortedStreamVideosList.get(selectedVideoStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareDescription(final String descriptionHtml) {
|
private void prepareDescription(final String descriptionHtml) {
|
||||||
|
@ -889,9 +964,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
}
|
}
|
||||||
|
|
||||||
disposables.add(Single.just(descriptionHtml)
|
disposables.add(Single.just(descriptionHtml)
|
||||||
.map(new Function<String, Spanned>() {
|
.map((@io.reactivex.annotations.NonNull String description) -> {
|
||||||
@Override
|
|
||||||
public Spanned apply(@io.reactivex.annotations.NonNull String description) throws Exception {
|
|
||||||
Spanned parsedDescription;
|
Spanned parsedDescription;
|
||||||
if (Build.VERSION.SDK_INT >= 24) {
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
parsedDescription = Html.fromHtml(description, 0);
|
parsedDescription = Html.fromHtml(description, 0);
|
||||||
|
@ -900,24 +973,23 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
parsedDescription = Html.fromHtml(description);
|
parsedDescription = Html.fromHtml(description);
|
||||||
}
|
}
|
||||||
return parsedDescription;
|
return parsedDescription;
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.subscribeOn(Schedulers.computation())
|
.subscribeOn(Schedulers.computation())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Spanned>() {
|
.subscribe((@io.reactivex.annotations.NonNull Spanned spanned) -> {
|
||||||
@Override
|
|
||||||
public void accept(@io.reactivex.annotations.NonNull Spanned spanned) throws Exception {
|
|
||||||
videoDescriptionView.setText(spanned);
|
videoDescriptionView.setText(spanned);
|
||||||
videoDescriptionView.setVisibility(View.VISIBLE);
|
videoDescriptionView.setVisibility(View.VISIBLE);
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private View getSeparatorView() {
|
private View getSeparatorView() {
|
||||||
View separator = new View(activity);
|
View separator = new View(activity);
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
|
LinearLayout.LayoutParams params =
|
||||||
int m8 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1);
|
||||||
int m5 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
|
int m8 = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
|
||||||
|
int m5 = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
|
||||||
params.setMargins(m8, m5, m8, m5);
|
params.setMargins(m8, m5, m8, m5);
|
||||||
separator.setLayoutParams(params);
|
separator.setLayoutParams(params);
|
||||||
|
|
||||||
|
@ -931,13 +1003,17 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
private void setHeightThumbnail() {
|
private void setHeightThumbnail() {
|
||||||
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
|
||||||
int height = isPortrait ? (int) (metrics.widthPixels / (16.0f / 9.0f)) : (int) (metrics.heightPixels / 2f);
|
int height = isPortrait
|
||||||
thumbnailImageView.setScaleType(isPortrait ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.FIT_CENTER);
|
? (int) (metrics.widthPixels / (16.0f / 9.0f))
|
||||||
thumbnailImageView.setLayoutParams(new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
|
: (int) (metrics.heightPixels / 2f);
|
||||||
|
thumbnailImageView.setLayoutParams(
|
||||||
|
new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
|
||||||
thumbnailImageView.setMinimumHeight(height);
|
thumbnailImageView.setMinimumHeight(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showContentWithAnimation(long duration, long delay, @FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
private void showContentWithAnimation(long duration,
|
||||||
|
long delay,
|
||||||
|
@FloatRange(from = 0.0f, to = 1.0f) float translationPercent) {
|
||||||
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
|
int translationY = (int) (getResources().getDisplayMetrics().heightPixels *
|
||||||
(translationPercent > 0.0f ? translationPercent : .06f));
|
(translationPercent > 0.0f ? translationPercent : .06f));
|
||||||
|
|
||||||
|
@ -945,23 +1021,38 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
contentRootLayoutHiding.setAlpha(0f);
|
contentRootLayoutHiding.setAlpha(0f);
|
||||||
contentRootLayoutHiding.setTranslationY(translationY);
|
contentRootLayoutHiding.setTranslationY(translationY);
|
||||||
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
contentRootLayoutHiding.setVisibility(View.VISIBLE);
|
||||||
contentRootLayoutHiding.animate().alpha(1f).translationY(0)
|
contentRootLayoutHiding.animate()
|
||||||
.setStartDelay(delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start();
|
.alpha(1f)
|
||||||
|
.translationY(0)
|
||||||
|
.setStartDelay(delay)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setInterpolator(new FastOutSlowInInterpolator())
|
||||||
|
.start();
|
||||||
|
|
||||||
uploaderRootLayout.animate().setListener(null).cancel();
|
uploaderRootLayout.animate().setListener(null).cancel();
|
||||||
uploaderRootLayout.setAlpha(0f);
|
uploaderRootLayout.setAlpha(0f);
|
||||||
uploaderRootLayout.setTranslationY(translationY);
|
uploaderRootLayout.setTranslationY(translationY);
|
||||||
uploaderRootLayout.setVisibility(View.VISIBLE);
|
uploaderRootLayout.setVisibility(View.VISIBLE);
|
||||||
uploaderRootLayout.animate().alpha(1f).translationY(0)
|
uploaderRootLayout.animate()
|
||||||
.setStartDelay((long) (duration * .5f) + delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start();
|
.alpha(1f)
|
||||||
|
.translationY(0)
|
||||||
|
.setStartDelay((long) (duration * .5f) + delay)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setInterpolator(new FastOutSlowInInterpolator())
|
||||||
|
.start();
|
||||||
|
|
||||||
if (showRelatedStreams) {
|
if (showRelatedStreams) {
|
||||||
relatedStreamRootLayout.animate().setListener(null).cancel();
|
relatedStreamRootLayout.animate().setListener(null).cancel();
|
||||||
relatedStreamRootLayout.setAlpha(0f);
|
relatedStreamRootLayout.setAlpha(0f);
|
||||||
relatedStreamRootLayout.setTranslationY(translationY);
|
relatedStreamRootLayout.setTranslationY(translationY);
|
||||||
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
||||||
relatedStreamRootLayout.animate().alpha(1f).translationY(0)
|
relatedStreamRootLayout.animate()
|
||||||
.setStartDelay((long) (duration * .8f) + delay).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()).start();
|
.alpha(1f)
|
||||||
|
.translationY(0)
|
||||||
|
.setStartDelay((long) (duration * .8f) + delay)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setInterpolator(new FastOutSlowInInterpolator())
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -975,12 +1066,8 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
if (thumbnailImageView == null || activity == null) return;
|
if (thumbnailImageView == null || activity == null) return;
|
||||||
|
|
||||||
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource));
|
thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, imageResource));
|
||||||
animateView(thumbnailImageView, false, 0, 0, new Runnable() {
|
animateView(thumbnailImageView, false, 0, 0,
|
||||||
@Override
|
() -> animateView(thumbnailImageView, true, 500));
|
||||||
public void run() {
|
|
||||||
animateView(thumbnailImageView, true, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1004,6 +1091,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
animateView(contentRootLayoutHiding, false, 200);
|
animateView(contentRootLayoutHiding, false, 200);
|
||||||
animateView(spinnerToolbar, false, 200);
|
animateView(spinnerToolbar, false, 200);
|
||||||
animateView(thumbnailPlayButton, false, 50);
|
animateView(thumbnailPlayButton, false, 50);
|
||||||
|
animateView(detailDurationView, false, 100);
|
||||||
|
|
||||||
videoTitleTextView.setText(name != null ? name : "");
|
videoTitleTextView.setText(name != null ? name : "");
|
||||||
videoTitleTextView.setMaxLines(1);
|
videoTitleTextView.setMaxLines(1);
|
||||||
|
@ -1033,6 +1121,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);
|
||||||
}
|
}
|
||||||
|
@ -1073,6 +1162,18 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
thumbsDisabledTextView.setVisibility(View.GONE);
|
thumbsDisabledTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.getDuration() > 0) {
|
||||||
|
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
|
||||||
|
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color));
|
||||||
|
animateView(detailDurationView, true, 100);
|
||||||
|
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
|
||||||
|
detailDurationView.setText(R.string.duration_live);
|
||||||
|
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
||||||
|
animateView(detailDurationView, true, 100);
|
||||||
|
} else {
|
||||||
|
detailDurationView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
videoTitleRoot.setClickable(true);
|
videoTitleRoot.setClickable(true);
|
||||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||||
|
@ -1084,7 +1185,7 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
prepareDescription(info.getDescription());
|
prepareDescription(info.getDescription());
|
||||||
|
|
||||||
animateView(spinnerToolbar, true, 500);
|
animateView(spinnerToolbar, true, 500);
|
||||||
setupActionBarHandler(info);
|
setupActionBar(info);
|
||||||
initThumbnailViews(info);
|
initThumbnailViews(info);
|
||||||
initRelatedVideos(info);
|
initRelatedVideos(info);
|
||||||
if (wasRelatedStreamsExpanded) {
|
if (wasRelatedStreamsExpanded) {
|
||||||
|
@ -1094,14 +1195,28 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
||||||
|
|
||||||
if (!info.getErrors().isEmpty()) {
|
if (!info.getErrors().isEmpty()) {
|
||||||
showSnackBarError(info.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(info.getServiceId()), info.getUrl(), 0);
|
showSnackBarError(info.getErrors(),
|
||||||
|
UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(info.getServiceId()),
|
||||||
|
info.getUrl(),
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
|
switch (info.getStreamType()) {
|
||||||
|
case LIVE_STREAM:
|
||||||
|
case AUDIO_LIVE_STREAM:
|
||||||
|
detailControlsDownload.setVisibility(View.GONE);
|
||||||
|
spinnerToolbar.setVisibility(View.GONE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!info.getVideoStreams().isEmpty()
|
||||||
|
|| !info.getVideoOnlyStreams().isEmpty()) break;
|
||||||
|
|
||||||
detailControlsBackground.setVisibility(View.GONE);
|
detailControlsBackground.setVisibility(View.GONE);
|
||||||
detailControlsPopup.setVisibility(View.GONE);
|
detailControlsPopup.setVisibility(View.GONE);
|
||||||
spinnerToolbar.setVisibility(View.GONE);
|
spinnerToolbar.setVisibility(View.GONE);
|
||||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoPlayEnabled) {
|
if (autoPlayEnabled) {
|
||||||
|
@ -1121,28 +1236,30 @@ public class VideoDetailFragment extends BaseStateFragment<StreamInfo> implement
|
||||||
|
|
||||||
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||||
onBlockedByGemaError();
|
onBlockedByGemaError();
|
||||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
|
||||||
showError(getString(R.string.live_streams_not_supported), false);
|
|
||||||
} else if (exception instanceof ContentNotAvailableException) {
|
} else if (exception instanceof ContentNotAvailableException) {
|
||||||
showError(getString(R.string.content_not_available), false);
|
showError(getString(R.string.content_not_available), false);
|
||||||
} else {
|
} else {
|
||||||
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error :
|
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException
|
||||||
exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
? R.string.youtube_signature_decryption_error
|
||||||
onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId);
|
: exception instanceof ParsingException
|
||||||
|
? R.string.parsing_error
|
||||||
|
: R.string.general_error;
|
||||||
|
onUnrecoverableError(exception,
|
||||||
|
UserAction.REQUESTED_STREAM,
|
||||||
|
NewPipe.getNameOfService(serviceId),
|
||||||
|
url,
|
||||||
|
errorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onBlockedByGemaError() {
|
public void onBlockedByGemaError() {
|
||||||
thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
thumbnailBackgroundButton.setOnClickListener((View v) -> {
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
Intent intent = new Intent();
|
Intent intent = new Intent();
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setData(Uri.parse(getString(R.string.c3s_url)));
|
intent.setData(Uri.parse(getString(R.string.c3s_url)));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema);
|
||||||
|
|
|
@ -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,12 +137,11 @@ 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);
|
||||||
NavigationHelper.openVideoDetailFragment(
|
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
|
||||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,30 +151,22 @@ 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);
|
||||||
NavigationHelper.openChannelFragment(
|
NavigationHelper.openChannelFragment(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);
|
||||||
NavigationHelper.openPlaylistFragment(
|
NavigationHelper.openPlaylistFragment(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 +191,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 +203,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();
|
||||||
|
@ -235,7 +227,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
if (supportActionBar != null) {
|
if (supportActionBar != null) {
|
||||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||||
if(useAsFrontPage) {
|
if (useAsFrontPage) {
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
@ -282,9 +274,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showListFooter(final boolean show) {
|
public void showListFooter(final boolean show) {
|
||||||
itemsList.post(new Runnable() {
|
itemsList.post(() -> {
|
||||||
@Override
|
if (infoListAdapter != null && itemsList != null) {
|
||||||
public void run() {
|
|
||||||
infoListAdapter.showFooter(show);
|
infoListAdapter.showFooter(show);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,8 @@ import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.functions.Consumer;
|
import io.reactivex.functions.Consumer;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
|
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||||
|
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
|
@ -29,7 +30,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
protected String url;
|
protected String url;
|
||||||
|
|
||||||
protected I currentInfo;
|
protected I currentInfo;
|
||||||
protected String currentNextItemsUrl;
|
protected String currentNextPageUrl;
|
||||||
protected Disposable currentWorker;
|
protected Disposable currentWorker;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -73,7 +74,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
public void writeTo(Queue<Object> objectsToSave) {
|
public void writeTo(Queue<Object> objectsToSave) {
|
||||||
super.writeTo(objectsToSave);
|
super.writeTo(objectsToSave);
|
||||||
objectsToSave.add(currentInfo);
|
objectsToSave.add(currentInfo);
|
||||||
objectsToSave.add(currentNextItemsUrl);
|
objectsToSave.add(currentNextPageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -81,7 +82,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||||
super.readFrom(savedObjects);
|
super.readFrom(savedObjects);
|
||||||
currentInfo = (I) savedObjects.poll();
|
currentInfo = (I) savedObjects.poll();
|
||||||
currentNextItemsUrl = (String) savedObjects.poll();
|
currentNextPageUrl = (String) savedObjects.poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -116,7 +117,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
.subscribe((@NonNull I result) -> {
|
.subscribe((@NonNull I result) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
currentInfo = result;
|
currentInfo = result;
|
||||||
currentNextItemsUrl = result.next_streams_url;
|
currentNextPageUrl = result.getNextPageUrl();
|
||||||
handleResult(result);
|
handleResult(result);
|
||||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||||
}
|
}
|
||||||
|
@ -125,7 +126,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
* Implement the logic to load more items<br/>
|
* Implement the logic to load more items<br/>
|
||||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
||||||
*/
|
*/
|
||||||
protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic();
|
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||||
|
|
||||||
protected void loadMoreItems() {
|
protected void loadMoreItems() {
|
||||||
isLoading.set(true);
|
isLoading.set(true);
|
||||||
|
@ -134,9 +135,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
currentWorker = loadMoreItemsLogic()
|
currentWorker = loadMoreItemsLogic()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) -> {
|
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
handleNextItems(nextItemsResult);
|
handleNextItems(InfoItemsPage);
|
||||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||||
isLoading.set(false);
|
isLoading.set(false);
|
||||||
onError(throwable);
|
onError(throwable);
|
||||||
|
@ -144,17 +145,17 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
currentNextItemsUrl = result.nextItemsUrl;
|
currentNextPageUrl = result.getNextPageUrl();
|
||||||
infoListAdapter.addInfoItemList(result.nextItemsList);
|
infoListAdapter.addInfoItemList(result.getItems());
|
||||||
|
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean hasMoreItems() {
|
protected boolean hasMoreItems() {
|
||||||
return !TextUtils.isEmpty(currentNextItemsUrl);
|
return !TextUtils.isEmpty(currentNextPageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -170,8 +171,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||||
setTitle(name);
|
setTitle(name);
|
||||||
|
|
||||||
if (infoListAdapter.getItemsList().size() == 0) {
|
if (infoListAdapter.getItemsList().size() == 0) {
|
||||||
if (result.related_streams.size() > 0) {
|
if (result.getRelatedItems().size() > 0) {
|
||||||
infoListAdapter.addInfoItemList(result.related_streams);
|
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||||
showListFooter(hasMoreItems());
|
showListFooter(hasMoreItems());
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
|
|
|
@ -27,23 +27,28 @@ import com.jakewharton.rxbinding2.view.RxView;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
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.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
|
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.fragments.list.BaseListInfoFragment;
|
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueue;
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.subscription.SubscriptionService;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
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 java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -108,11 +113,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
subscriptionService = SubscriptionService.getInstance();
|
subscriptionService = SubscriptionService.getInstance(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
||||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,17 +199,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 +227,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);
|
||||||
}
|
}
|
||||||
|
@ -392,8 +393,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
|
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -419,8 +420,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
super.handleResult(result);
|
super.handleResult(result);
|
||||||
|
|
||||||
headerRootLayout.setVisibility(View.VISIBLE);
|
headerRootLayout.setVisibility(View.VISIBLE);
|
||||||
imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
|
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||||
imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
|
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||||
|
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||||
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
|
|
||||||
if (result.getSubscriberCount() != -1) {
|
if (result.getSubscriberCount() != -1) {
|
||||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
||||||
|
@ -428,10 +431,11 @@ 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.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disposables != null) disposables.clear();
|
if (disposables != null) disposables.clear();
|
||||||
|
@ -439,24 +443,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
updateSubscription(result);
|
updateSubscription(result);
|
||||||
monitorSubscription(result);
|
monitorSubscription(result);
|
||||||
|
|
||||||
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
|
headerPlayAllButton.setOnClickListener(
|
||||||
@Override
|
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||||
public void onClick(View view) {
|
headerPopupButton.setOnClickListener(
|
||||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||||
}
|
headerBackgroundButton.setOnClickListener(
|
||||||
});
|
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||||
headerPopupButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue() {
|
private PlayQueue getPlayQueue() {
|
||||||
|
@ -464,17 +456,23 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue(final int index) {
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
|
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||||
|
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||||
|
if(i instanceof StreamInfoItem) {
|
||||||
|
streamItems.add((StreamInfoItem) i);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ChannelPlayQueue(
|
return new ChannelPlayQueue(
|
||||||
currentInfo.getServiceId(),
|
currentInfo.getServiceId(),
|
||||||
currentInfo.getUrl(),
|
currentInfo.getUrl(),
|
||||||
currentInfo.getNextStreamsUrl(),
|
currentInfo.getNextPageUrl(),
|
||||||
infoListAdapter.getItemsList(),
|
streamItems,
|
||||||
index
|
index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
|
|
@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
|
||||||
import org.schabi.newpipe.report.UserAction;
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.subscription.SubscriptionService;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -64,7 +64,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
subscriptionService = SubscriptionService.getInstance();
|
subscriptionService = SubscriptionService.getInstance(activity);
|
||||||
|
|
||||||
FEED_LOAD_COUNT = howManyItemsToLoad();
|
FEED_LOAD_COUNT = howManyItemsToLoad();
|
||||||
}
|
}
|
||||||
|
@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||||
// Called only when response is non-empty
|
// Called only when response is non-empty
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(final ChannelInfo channelInfo) {
|
public void onSuccess(final ChannelInfo channelInfo) {
|
||||||
if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) {
|
if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) {
|
||||||
onDone();
|
onDone();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final InfoItem item = channelInfo.getRelatedStreams().get(0);
|
final InfoItem item = channelInfo.getRelatedItems().get(0);
|
||||||
// Keep requesting new items if the current one already exists
|
// Keep requesting new items if the current one already exists
|
||||||
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
|
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
|
||||||
if (!itemExists) {
|
if (!itemExists) {
|
||||||
|
@ -411,7 +411,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||||
|
|
||||||
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
|
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
|
||||||
for (final InfoItem existingItem : items) {
|
for (final InfoItem existingItem : items) {
|
||||||
if (existingItem.info_type == item.info_type &&
|
if (existingItem.getInfoType() == item.getInfoType() &&
|
||||||
existingItem.getServiceId() == item.getServiceId() &&
|
existingItem.getServiceId() == item.getServiceId() &&
|
||||||
existingItem.getName().equals(item.getName()) &&
|
existingItem.getName().equals(item.getName()) &&
|
||||||
existingItem.getUrl().equals(item.getUrl())) return true;
|
existingItem.getUrl().equals(item.getUrl())) return true;
|
||||||
|
|
|
@ -141,12 +141,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
String contentCountry = PreferenceManager
|
String contentCountry = PreferenceManager
|
||||||
.getDefaultSharedPreferences(activity)
|
.getDefaultSharedPreferences(activity)
|
||||||
.getString(getString(R.string.content_country_key),
|
.getString(getString(R.string.content_country_key),
|
||||||
getString(R.string.default_country_value));
|
getString(R.string.default_country_value));
|
||||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextItemsUrl, contentCountry);
|
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -174,7 +174,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
|
|
@ -17,27 +17,50 @@ 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.InfoItem;
|
||||||
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.StreamInfo;
|
||||||
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;
|
||||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
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.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
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 +77,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 +90,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 +120,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 +145,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 +166,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 +173,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -151,8 +210,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
|
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -166,10 +225,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,42 +262,40 @@ 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())
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistCtrl.setVisibility(View.VISIBLE);
|
playlistCtrl.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
|
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
|
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||||
|
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
|
||||||
|
(int) result.getStreamCount(), (int) result.getStreamCount()));
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
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() {
|
||||||
|
@ -244,17 +303,23 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayQueue getPlayQueue(final int index) {
|
private PlayQueue getPlayQueue(final int index) {
|
||||||
|
final List<StreamInfoItem> infoItems = new ArrayList<>();
|
||||||
|
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||||
|
if(i instanceof StreamInfoItem) {
|
||||||
|
infoItems.add((StreamInfoItem) i);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new PlaylistPlayQueue(
|
return new PlaylistPlayQueue(
|
||||||
currentInfo.getServiceId(),
|
currentInfo.getServiceId(),
|
||||||
currentInfo.getUrl(),
|
currentInfo.getUrl(),
|
||||||
currentInfo.getNextStreamsUrl(),
|
currentInfo.getNextPageUrl(),
|
||||||
infoListAdapter.getItemsList(),
|
infoItems,
|
||||||
index
|
index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
super.handleNextItems(result);
|
super.handleNextItems(result);
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
|
@ -280,9 +345,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,22 +61,19 @@ 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;
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
|
public class SearchFragment
|
||||||
|
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage>
|
||||||
|
implements BackPressable {
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Search
|
// Search
|
||||||
|
@ -121,7 +115,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 +161,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 +529,27 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||||
|
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
|
||||||
|
searchEditText == null || disposables == null) return;
|
||||||
|
final String query = item.query;
|
||||||
new AlertDialog.Builder(activity)
|
new AlertDialog.Builder(activity)
|
||||||
.setTitle(item.query)
|
.setTitle(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) -> {
|
||||||
@Override
|
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||||
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())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<Integer>() {
|
.subscribe(
|
||||||
@Override
|
howManyDeleted -> suggestionPublisher
|
||||||
public void accept(Integer howManyDeleted) throws Exception {
|
.onNext(searchEditText.getText().toString()),
|
||||||
suggestionPublisher.onNext(searchEditText.getText().toString());
|
throwable -> showSnackBarError(throwable,
|
||||||
}
|
UserAction.SOMETHING_ELSE, "none",
|
||||||
}, new Consumer<Throwable>() {
|
"Deleting item failed", R.string.general_error)
|
||||||
@Override
|
);
|
||||||
public void accept(Throwable throwable) throws Exception {
|
disposables.add(onDelete);
|
||||||
showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error);
|
})
|
||||||
}
|
.show();
|
||||||
}));
|
|
||||||
}
|
|
||||||
}).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -589,29 +574,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 +593,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 +622,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 +687,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,19 +706,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<SearchResult>() {
|
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||||
@Override
|
.subscribe(this::handleResult, this::onError);
|
||||||
public void accept(@NonNull SearchResult result) throws Exception {
|
|
||||||
isLoading.set(false);
|
|
||||||
handleResult(result);
|
|
||||||
}
|
|
||||||
}, new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
|
||||||
isLoading.set(false);
|
|
||||||
onError(throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -758,19 +719,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
|
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||||
@Override
|
.subscribe(this::handleNextItems, this::onError);
|
||||||
public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
|
|
||||||
isLoading.set(false);
|
|
||||||
handleNextItems(result);
|
|
||||||
}
|
|
||||||
}, new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
|
||||||
isLoading.set(false);
|
|
||||||
onError(throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -811,12 +761,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
||||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||||
suggestionsRecyclerView.post(new Runnable() {
|
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
suggestionListAdapter.setItems(suggestions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
@ -874,10 +819,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
currentPage = Integer.parseInt(result.getNextItemsUrl());
|
currentPage = Integer.parseInt(result.getNextPageUrl());
|
||||||
infoListAdapter.addInfoItemList(result.getNextItemsList());
|
infoListAdapter.addInfoItemList(result.getItems());
|
||||||
|
|
||||||
if (!result.getErrors().isEmpty()) {
|
if (!result.getErrors().isEmpty()) {
|
||||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
||||||
|
|
|
@ -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,60 @@
|
||||||
|
package org.schabi.newpipe.fragments.local;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
|
|
||||||
|
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,178 @@
|
||||||
|
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) {
|
||||||
|
if (itemsList == null) return;
|
||||||
|
itemsList.post(() -> {
|
||||||
|
if (itemListAdapter != null) 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,161 @@
|
||||||
|
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 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,42 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem;
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
|
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
|
|
||||||
|
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,
|
||||||
|
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, dateFormat);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
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 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.ImageDisplayConstants;
|
||||||
|
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,
|
||||||
|
ImageDisplayConstants.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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
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 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.ImageDisplayConstants;
|
||||||
|
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,
|
||||||
|
ImageDisplayConstants.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.schabi.newpipe.fragments.local.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
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.ImageDisplayConstants;
|
||||||
|
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,
|
||||||
|
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||||
|
|
||||||
|
super.updateFromItem(localItem, dateFormat);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package org.schabi.newpipe.fragments.subscription;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.DialogFragment;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import icepick.Icepick;
|
||||||
|
import icepick.State;
|
||||||
|
|
||||||
|
public class ImportConfirmationDialog extends DialogFragment {
|
||||||
|
@State
|
||||||
|
protected Intent resultServiceIntent;
|
||||||
|
|
||||||
|
public void setResultServiceIntent(Intent resultServiceIntent) {
|
||||||
|
this.resultServiceIntent = resultServiceIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) {
|
||||||
|
if (fragment.getFragmentManager() == null) return;
|
||||||
|
|
||||||
|
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
|
||||||
|
confirmationDialog.setResultServiceIntent(resultServiceIntent);
|
||||||
|
confirmationDialog.show(fragment.getFragmentManager(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
|
return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext()))
|
||||||
|
.setMessage(R.string.import_network_expensive_warning)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||||
|
if (resultServiceIntent != null && getContext() != null) {
|
||||||
|
getContext().startService(resultServiceIntent);
|
||||||
|
}
|
||||||
|
dismiss();
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null");
|
||||||
|
|
||||||
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
Icepick.saveInstanceState(this, outState);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,62 @@
|
||||||
package org.schabi.newpipe.fragments.subscription;
|
package org.schabi.newpipe.fragments.subscription;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
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.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
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.subscription.SubscriptionService;
|
||||||
|
import org.schabi.newpipe.subscription.services.SubscriptionsExportService;
|
||||||
|
import org.schabi.newpipe.subscription.services.SubscriptionsImportService;
|
||||||
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.views.CollapsibleView;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Observer;
|
import io.reactivex.Observer;
|
||||||
|
@ -33,18 +65,29 @@ import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.disposables.Disposable;
|
import io.reactivex.disposables.Disposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE;
|
||||||
|
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
||||||
private View headerRootLayout;
|
private static final int REQUEST_EXPORT_CODE = 666;
|
||||||
|
private static final int REQUEST_IMPORT_CODE = 667;
|
||||||
|
|
||||||
private InfoListAdapter infoListAdapter;
|
|
||||||
private RecyclerView itemsList;
|
private RecyclerView itemsList;
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected Parcelable itemsListState;
|
protected Parcelable itemsListState;
|
||||||
|
private InfoListAdapter infoListAdapter;
|
||||||
|
|
||||||
|
private View headerRootLayout;
|
||||||
|
private View whatsNewItemListHeader;
|
||||||
|
private View importExportListHeader;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected Parcelable importExportOptionsState;
|
||||||
|
private CollapsibleView importExportOptions;
|
||||||
|
|
||||||
/* Used for independent events */
|
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
private SubscriptionService subscriptionService;
|
private SubscriptionService subscriptionService;
|
||||||
|
|
||||||
|
@ -52,39 +95,48 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
// Fragment LifeCycle
|
// Fragment LifeCycle
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
super.setUserVisibleHint(isVisibleToUser);
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
if(isVisibleToUser && activity != null) {
|
if (isVisibleToUser) {
|
||||||
activity.getSupportActionBar()
|
setTitle(getString(R.string.tab_subscriptions));
|
||||||
.setTitle(R.string.tab_subscriptions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
infoListAdapter = new InfoListAdapter(activity);
|
infoListAdapter = new InfoListAdapter(activity);
|
||||||
subscriptionService = SubscriptionService.getInstance();
|
subscriptionService = SubscriptionService.getInstance(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
|
||||||
activity.setTitle(R.string.tab_subscriptions);
|
|
||||||
if(useAsFrontPage) {
|
|
||||||
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
}
|
|
||||||
return inflater.inflate(R.layout.fragment_subscription, container, false);
|
return inflater.inflate(R.layout.fragment_subscription, container, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
setupBroadcastReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||||
|
importExportOptionsState = importExportOptions.onSaveInstanceState();
|
||||||
|
|
||||||
|
if (subscriptionBroadcastReceiver != null && activity != null) {
|
||||||
|
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -103,9 +155,131 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
/*/////////////////////////////////////////////////////////////////////////
|
||||||
|
// Menu
|
||||||
|
/////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
|
||||||
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
|
if (supportActionBar != null) {
|
||||||
|
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||||
|
setTitle(getString(R.string.tab_subscriptions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Subscriptions import/export
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private BroadcastReceiver subscriptionBroadcastReceiver;
|
||||||
|
|
||||||
|
private void setupBroadcastReceiver() {
|
||||||
|
if (activity == null) return;
|
||||||
|
|
||||||
|
if (subscriptionBroadcastReceiver != null) {
|
||||||
|
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
final IntentFilter filters = new IntentFilter();
|
||||||
|
filters.addAction(SubscriptionsExportService.EXPORT_COMPLETE_ACTION);
|
||||||
|
filters.addAction(SubscriptionsImportService.IMPORT_COMPLETE_ACTION);
|
||||||
|
subscriptionBroadcastReceiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (importExportOptions != null) importExportOptions.collapse();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View addItemView(final String title, @DrawableRes final int icon, ViewGroup container) {
|
||||||
|
final View itemRoot = View.inflate(getContext(), R.layout.subscription_import_export_item, null);
|
||||||
|
final TextView titleView = itemRoot.findViewById(android.R.id.text1);
|
||||||
|
final ImageView iconView = itemRoot.findViewById(android.R.id.icon1);
|
||||||
|
|
||||||
|
titleView.setText(title);
|
||||||
|
iconView.setImageResource(icon);
|
||||||
|
|
||||||
|
container.addView(itemRoot);
|
||||||
|
return itemRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupImportFromItems(final ViewGroup listHolder) {
|
||||||
|
final View previousBackupItem = addItemView(getString(R.string.previous_export), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder);
|
||||||
|
previousBackupItem.setOnClickListener(item -> onImportPreviousSelected());
|
||||||
|
|
||||||
|
final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE;
|
||||||
|
final String[] services = getResources().getStringArray(R.array.service_list);
|
||||||
|
for (String serviceName : services) {
|
||||||
|
try {
|
||||||
|
final StreamingService service = NewPipe.getService(serviceName);
|
||||||
|
|
||||||
|
final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor();
|
||||||
|
if (subscriptionExtractor == null) continue;
|
||||||
|
|
||||||
|
final List<SubscriptionExtractor.ContentSource> supportedSources = subscriptionExtractor.getSupportedSources();
|
||||||
|
if (supportedSources.isEmpty()) continue;
|
||||||
|
|
||||||
|
final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder);
|
||||||
|
final ImageView iconView = itemView.findViewById(android.R.id.icon1);
|
||||||
|
iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
|
||||||
|
|
||||||
|
itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId()));
|
||||||
|
} catch (ExtractionException e) {
|
||||||
|
throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupExportToItems(final ViewGroup listHolder) {
|
||||||
|
final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder);
|
||||||
|
previousBackupItem.setOnClickListener(item -> onExportSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onImportFromServiceSelected(int serviceId) {
|
||||||
|
if (getParentFragment() == null) return;
|
||||||
|
NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onImportPreviousSelected() {
|
||||||
|
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onExportSelected() {
|
||||||
|
final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date());
|
||||||
|
final String exportName = "newpipe_subscriptions_" + date + ".json";
|
||||||
|
final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName);
|
||||||
|
|
||||||
|
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (requestCode == REQUEST_EXPORT_CODE) {
|
||||||
|
final File exportFile = Utils.getFileForUri(data.getData());
|
||||||
|
if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) {
|
||||||
|
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
activity.startService(new Intent(activity, SubscriptionsExportService.class)
|
||||||
|
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
} else if (requestCode == REQUEST_IMPORT_CODE) {
|
||||||
|
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||||
|
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||||
|
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
||||||
|
.putExtra(KEY_VALUE, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*/////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment Views
|
// Fragment Views
|
||||||
///////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
@ -116,33 +290,46 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
|
|
||||||
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
||||||
infoListAdapter.useMiniItemVariants(true);
|
whatsNewItemListHeader = headerRootLayout.findViewById(R.id.whats_new);
|
||||||
|
importExportListHeader = headerRootLayout.findViewById(R.id.import_export);
|
||||||
|
importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
|
||||||
|
|
||||||
|
infoListAdapter.useMiniItemVariants(true);
|
||||||
itemsList.setAdapter(infoListAdapter);
|
itemsList.setAdapter(infoListAdapter);
|
||||||
|
|
||||||
|
setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
|
||||||
|
setupExportToItems(headerRootLayout.findViewById(R.id.export_to_options));
|
||||||
|
|
||||||
|
if (importExportOptionsState != null) {
|
||||||
|
importExportOptions.onRestoreInstanceState(importExportOptionsState);
|
||||||
|
importExportOptionsState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon)));
|
||||||
|
importExportOptions.ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) {
|
||||||
|
return newState -> animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
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.getUrl(), selectedItem.getName());
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void held(ChannelInfoItem selectedItem) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
headerRootLayout.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
whatsNewItemListHeader.setOnClickListener(v ->
|
||||||
|
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
|
||||||
|
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetFragment() {
|
private void resetFragment() {
|
||||||
|
@ -196,6 +383,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
infoListAdapter.clearStreamItemList();
|
infoListAdapter.clearStreamItemList();
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
|
whatsNewItemListHeader.setVisibility(View.GONE);
|
||||||
showEmptyState();
|
showEmptyState();
|
||||||
} else {
|
} else {
|
||||||
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
|
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
|
||||||
|
@ -203,7 +391,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||||
itemsListState = null;
|
itemsListState = null;
|
||||||
}
|
}
|
||||||
|
whatsNewItemListHeader.setVisibility(View.VISIBLE);
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,12 +401,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
List<InfoItem> items = new ArrayList<>();
|
List<InfoItem> items = new ArrayList<>();
|
||||||
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
||||||
|
|
||||||
Collections.sort(items, new Comparator<InfoItem>() {
|
Collections.sort(items,
|
||||||
@Override
|
(InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
|
||||||
public int compare(InfoItem o1, InfoItem o2) {
|
|
||||||
return o1.name.compareToIgnoreCase(o2.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,12 +422,6 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||||
animateView(itemsList, true, 200);
|
animateView(itemsList, true, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showEmptyState() {
|
|
||||||
super.showEmptyState();
|
|
||||||
animateView(itemsList, false, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Fragment Error Handling
|
// Fragment Error Handling
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
package org.schabi.newpipe.fragments.subscription;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.v4.text.util.LinkifyCompat;
|
||||||
|
import android.support.v7.app.ActionBar;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.util.Linkify;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.nononsenseapps.filepicker.Utils;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.BaseFragment;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||||
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
import org.schabi.newpipe.report.UserAction;
|
||||||
|
import org.schabi.newpipe.subscription.services.SubscriptionsImportService;
|
||||||
|
import org.schabi.newpipe.util.Constants;
|
||||||
|
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.State;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||||
|
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||||
|
|
||||||
|
public class SubscriptionsImportFragment extends BaseFragment {
|
||||||
|
private static final int REQUEST_IMPORT_FILE_CODE = 666;
|
||||||
|
|
||||||
|
@State
|
||||||
|
protected int currentServiceId = Constants.NO_SERVICE_ID;
|
||||||
|
|
||||||
|
private List<SubscriptionExtractor.ContentSource> supportedSources;
|
||||||
|
private String relatedUrl;
|
||||||
|
@StringRes
|
||||||
|
private int instructionsString;
|
||||||
|
|
||||||
|
public static SubscriptionsImportFragment getInstance(int serviceId) {
|
||||||
|
SubscriptionsImportFragment instance = new SubscriptionsImportFragment();
|
||||||
|
instance.setInitialData(serviceId);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInitialData(int serviceId) {
|
||||||
|
this.currentServiceId = serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Views
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
private TextView infoTextView;
|
||||||
|
|
||||||
|
private EditText inputText;
|
||||||
|
private Button inputButton;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment LifeCycle
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setupServiceVariables();
|
||||||
|
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||||
|
ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||||
|
NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error));
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||||
|
super.setUserVisibleHint(isVisibleToUser);
|
||||||
|
if (isVisibleToUser) {
|
||||||
|
setTitle(getString(R.string.import_title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_import, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*/////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment Views
|
||||||
|
/////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||||
|
super.initViews(rootView, savedInstanceState);
|
||||||
|
|
||||||
|
inputButton = rootView.findViewById(R.id.input_button);
|
||||||
|
inputText = rootView.findViewById(R.id.input_text);
|
||||||
|
|
||||||
|
infoTextView = rootView.findViewById(R.id.info_text_view);
|
||||||
|
|
||||||
|
// TODO: Support services that can import from more than one source (show the option to the user)
|
||||||
|
if (supportedSources.contains(CHANNEL_URL)) {
|
||||||
|
inputButton.setText(R.string.import_title);
|
||||||
|
inputText.setVisibility(View.VISIBLE);
|
||||||
|
inputText.setHint(ServiceHelper.getImportInstructionsHint(currentServiceId));
|
||||||
|
} else {
|
||||||
|
inputButton.setText(R.string.import_file_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instructionsString != 0) {
|
||||||
|
if (TextUtils.isEmpty(relatedUrl)) {
|
||||||
|
setInfoText(getString(instructionsString));
|
||||||
|
} else {
|
||||||
|
setInfoText(getString(instructionsString, relatedUrl));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setInfoText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||||
|
if (supportActionBar != null) {
|
||||||
|
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||||
|
setTitle(getString(R.string.import_title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initListeners() {
|
||||||
|
super.initListeners();
|
||||||
|
inputButton.setOnClickListener(v -> onImportClicked());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onImportClicked() {
|
||||||
|
if (inputText.getVisibility() == View.VISIBLE) {
|
||||||
|
final String value = inputText.getText().toString();
|
||||||
|
if (!value.isEmpty()) onImportUrl(value);
|
||||||
|
} else {
|
||||||
|
onImportFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onImportUrl(String value) {
|
||||||
|
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||||
|
.putExtra(KEY_MODE, CHANNEL_URL_MODE)
|
||||||
|
.putExtra(KEY_VALUE, value)
|
||||||
|
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onImportFile() {
|
||||||
|
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) {
|
||||||
|
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||||
|
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||||
|
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
|
||||||
|
.putExtra(KEY_VALUE, path)
|
||||||
|
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// Subscriptions
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void setupServiceVariables() {
|
||||||
|
if (currentServiceId != Constants.NO_SERVICE_ID) {
|
||||||
|
try {
|
||||||
|
final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor();
|
||||||
|
supportedSources = extractor.getSupportedSources();
|
||||||
|
relatedUrl = extractor.getRelatedUrl();
|
||||||
|
instructionsString = ServiceHelper.getImportInstructions(currentServiceId);
|
||||||
|
return;
|
||||||
|
} catch (ExtractionException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedSources = Collections.emptyList();
|
||||||
|
relatedUrl = null;
|
||||||
|
instructionsString = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInfoText(String infoString) {
|
||||||
|
infoTextView.setText(infoString);
|
||||||
|
LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,171 @@
|
||||||
|
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.ImageDisplayConstants;
|
||||||
|
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,
|
||||||
|
ImageDisplayConstants.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;
|
||||||
|
@ -63,7 +60,7 @@ public class InfoItemBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
||||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
|
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||||
holder.updateFromItem(infoItem);
|
holder.updateFromItem(infoItem);
|
||||||
return holder.itemView;
|
return holder.itemView;
|
||||||
}
|
}
|
||||||
|
@ -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.getInfoType()) {
|
||||||
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:
|
||||||
|
|
|
@ -44,15 +44,16 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||||
|
|
||||||
itemChannelDescriptionView.setText(item.description);
|
itemChannelDescriptionView.setText(item.getDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getDetailLine(final ChannelInfoItem item) {
|
protected String getDetailLine(final ChannelInfoItem item) {
|
||||||
String details = super.getDetailLine(item);
|
String details = super.getDetailLine(item);
|
||||||
|
|
||||||
if (item.stream_count >= 0) {
|
if (item.getStreamCount() >= 0) {
|
||||||
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count);
|
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(),
|
||||||
|
item.getStreamCount());
|
||||||
|
|
||||||
if (!details.isEmpty()) {
|
if (!details.isEmpty()) {
|
||||||
details += " • " + formattedVideoAmount;
|
details += " • " + formattedVideoAmount;
|
||||||
|
|
|
@ -1,15 +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.TextView;
|
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.InfoItem;
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
import de.hdodenhof.circleimageview.CircleImageView;
|
import de.hdodenhof.circleimageview.CircleImageView;
|
||||||
|
@ -40,34 +38,23 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
|
|
||||||
itemBuilder.getImageLoader()
|
itemBuilder.getImageLoader()
|
||||||
.displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
.displayImage(item.getThumbnailUrl(),
|
||||||
|
itemThumbnailView,
|
||||||
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
itemView.setOnClickListener(new View.OnClickListener() {
|
itemView.setOnClickListener(view -> {
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
itemBuilder.getOnChannelSelectedListener().selected(item);
|
itemBuilder.getOnChannelSelectedListener().selected(item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getDetailLine(final ChannelInfoItem item) {
|
protected String getDetailLine(final ChannelInfoItem item) {
|
||||||
String details = "";
|
String details = "";
|
||||||
if (item.subscriber_count >= 0) {
|
if (item.getSubscriberCount() >= 0) {
|
||||||
details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count);
|
details += Localization.shortSubscriberCount(itemBuilder.getContext(),
|
||||||
|
item.getSubscriberCount());
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display options for channel thumbnails
|
|
||||||
*/
|
|
||||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
|
||||||
.showImageOnLoading(R.drawable.buddy_channel_item)
|
|
||||||
.showImageForEmptyUri(R.drawable.buddy_channel_item)
|
|
||||||
.showImageOnFail(R.drawable.buddy_channel_item)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
import org.schabi.newpipe.extractor.InfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
|
||||||
|
@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void updateFromItem(final InfoItem infoItem);
|
public abstract void updateFromItem(final InfoItem infoItem);
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// ImageLoaderOptions
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base display options
|
|
||||||
*/
|
|
||||||
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
|
||||||
new DisplayImageOptions.Builder()
|
|
||||||
.cacheInMemory(true)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,59 @@
|
||||||
|
package org.schabi.newpipe.info_list.holder;
|
||||||
|
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
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.util.ImageDisplayConstants;
|
||||||
|
|
||||||
|
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.getThumbnailUrl(), itemThumbnailView,
|
||||||
|
ImageDisplayConstants.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,14 +51,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||||
|
|
||||||
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
||||||
String viewsAndDate = "";
|
String viewsAndDate = "";
|
||||||
if (infoItem.view_count >= 0) {
|
if (infoItem.getViewCount() >= 0) {
|
||||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
|
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(infoItem.upload_date)) {
|
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
|
||||||
if (viewsAndDate.isEmpty()) {
|
if (viewsAndDate.isEmpty()) {
|
||||||
viewsAndDate = infoItem.upload_date;
|
viewsAndDate = infoItem.getUploadDate();
|
||||||
} else {
|
} else {
|
||||||
viewsAndDate += " • " + infoItem.upload_date;
|
viewsAndDate += " • " + infoItem.getUploadDate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return viewsAndDate;
|
return viewsAndDate;
|
||||||
|
|
|
@ -6,13 +6,12 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
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.InfoItem;
|
||||||
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.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
|
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
|
||||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
@ -41,15 +40,17 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||||
|
|
||||||
itemVideoTitleView.setText(item.getName());
|
itemVideoTitleView.setText(item.getName());
|
||||||
itemUploaderView.setText(item.uploader_name);
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
if (item.duration > 0) {
|
if (item.getDuration() > 0) {
|
||||||
itemDurationView.setText(Localization.getDurationString(item.duration));
|
itemDurationView.setText(Localization.getDurationString(item.getDuration()));
|
||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
|
R.color.duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
} else if (item.stream_type == StreamType.LIVE_STREAM) {
|
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||||
itemDurationView.setText(R.string.duration_live);
|
itemDurationView.setText(R.string.duration_live);
|
||||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
|
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||||
|
R.color.live_duration_background_color));
|
||||||
itemDurationView.setVisibility(View.VISIBLE);
|
itemDurationView.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
itemDurationView.setVisibility(View.GONE);
|
itemDurationView.setVisibility(View.GONE);
|
||||||
|
@ -57,25 +58,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
itemBuilder.getImageLoader()
|
itemBuilder.getImageLoader()
|
||||||
.displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
.displayImage(item.getThumbnailUrl(),
|
||||||
|
itemThumbnailView,
|
||||||
|
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||||
|
|
||||||
itemView.setOnClickListener(new View.OnClickListener() {
|
itemView.setOnClickListener(view -> {
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (item.stream_type) {
|
switch (item.getStreamType()) {
|
||||||
case AUDIO_STREAM:
|
case AUDIO_STREAM:
|
||||||
case VIDEO_STREAM:
|
case VIDEO_STREAM:
|
||||||
case FILE:
|
|
||||||
enableLongClick(item);
|
|
||||||
break;
|
|
||||||
case LIVE_STREAM:
|
case LIVE_STREAM:
|
||||||
case AUDIO_LIVE_STREAM:
|
case AUDIO_LIVE_STREAM:
|
||||||
|
enableLongClick(item);
|
||||||
|
break;
|
||||||
|
case FILE:
|
||||||
case NONE:
|
case NONE:
|
||||||
default:
|
default:
|
||||||
disableLongClick();
|
disableLongClick();
|
||||||
|
@ -85,14 +85,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
|
|
||||||
private void enableLongClick(final StreamInfoItem item) {
|
private void enableLongClick(final StreamInfoItem item) {
|
||||||
itemView.setLongClickable(true);
|
itemView.setLongClickable(true);
|
||||||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
itemView.setOnLongClickListener(view -> {
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View view) {
|
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
itemBuilder.getOnStreamSelectedListener().held(item);
|
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||||
itemView.setLongClickable(false);
|
itemView.setLongClickable(false);
|
||||||
itemView.setOnLongClickListener(null);
|
itemView.setOnLongClickListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.RemoteViews;
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
@ -46,6 +47,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||||
import org.schabi.newpipe.player.helper.LockManager;
|
import org.schabi.newpipe.player.helper.LockManager;
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
@ -75,6 +77,7 @@ public final class BackgroundPlayer extends Service {
|
||||||
|
|
||||||
private BasePlayerImpl basePlayerImpl;
|
private BasePlayerImpl basePlayerImpl;
|
||||||
private LockManager lockManager;
|
private LockManager lockManager;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Service-Activity Binder
|
// Service-Activity Binder
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -291,15 +294,15 @@ public final class BackgroundPlayer extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||||
super.onThumbnailReceived(thumbnail);
|
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||||
|
|
||||||
if (thumbnail != null) {
|
if (loadedImage != null) {
|
||||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||||
resetNotification();
|
resetNotification();
|
||||||
|
|
||||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||||
|
|
||||||
updateNotification(-1);
|
updateNotification(-1);
|
||||||
}
|
}
|
||||||
|
@ -378,29 +381,34 @@ public final class BackgroundPlayer extends Service {
|
||||||
// Playback Listener
|
// Playback Listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
@Nullable final StreamInfo info,
|
||||||
if (currentItem == item && currentInfo == info) return;
|
final int newPlayQueueIndex,
|
||||||
super.sync(item, info);
|
final boolean hasPlayQueueItemChanged) {
|
||||||
|
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) {
|
||||||
resetNotification();
|
resetNotification();
|
||||||
updateNotification(-1);
|
updateNotification(-1);
|
||||||
updateMetadata();
|
updateMetadata();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||||
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
|
final MediaSource liveSource = super.sourceOf(item, info);
|
||||||
if (index < 0 || index >= info.audio_streams.size()) return null;
|
if (liveSource != null) return liveSource;
|
||||||
|
|
||||||
final AudioStream audio = info.audio_streams.get(index);
|
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||||
return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId()));
|
if (index < 0 || index >= info.getAudioStreams().size()) return null;
|
||||||
|
|
||||||
|
final AudioStream audio = info.getAudioStreams().get(index);
|
||||||
|
return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
||||||
|
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void onPlaybackShutdown() {
|
||||||
super.shutdown();
|
super.onPlaybackShutdown();
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,7 +437,8 @@ public final class BackgroundPlayer extends Service {
|
||||||
|
|
||||||
private void updatePlayback() {
|
private void updatePlayback() {
|
||||||
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
||||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters());
|
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
||||||
|
playQueue.isShuffled(), getPlaybackParameters());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +486,7 @@ public final class BackgroundPlayer extends Service {
|
||||||
onClose();
|
onClose();
|
||||||
break;
|
break;
|
||||||
case ACTION_PLAY_PAUSE:
|
case ACTION_PLAY_PAUSE:
|
||||||
onVideoPlayPause();
|
onPlayPause();
|
||||||
break;
|
break;
|
||||||
case ACTION_REPEAT:
|
case ACTION_REPEAT:
|
||||||
onRepeatClicked();
|
onRepeatClicked();
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
package org.schabi.newpipe.player;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -33,9 +32,12 @@ import android.preference.PreferenceManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -48,35 +50,48 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||||
|
|
||||||
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.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||||
|
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueue;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
|
||||||
import org.schabi.newpipe.util.AnimationUtils;
|
import org.schabi.newpipe.util.AnimationUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.PopupMenuIconHacker;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||||
|
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||||
|
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||||
|
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
|
||||||
|
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity Player implementing VideoPlayer
|
* Activity Player implementing VideoPlayer
|
||||||
*
|
*
|
||||||
* @author mauriciocolli
|
* @author mauriciocolli
|
||||||
*/
|
*/
|
||||||
public final class MainVideoPlayer extends Activity {
|
public final class MainVideoPlayer extends AppCompatActivity
|
||||||
|
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
|
||||||
private static final String TAG = ".MainVideoPlayer";
|
private static final String TAG = ".MainVideoPlayer";
|
||||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||||
private static final String PLAYER_STATE_INTENT = "player_state_intent";
|
|
||||||
|
|
||||||
private GestureDetector gestureDetector;
|
private GestureDetector gestureDetector;
|
||||||
|
|
||||||
|
@ -85,6 +100,8 @@ public final class MainVideoPlayer extends Activity {
|
||||||
|
|
||||||
private SharedPreferences defaultPreferences;
|
private SharedPreferences defaultPreferences;
|
||||||
|
|
||||||
|
@Nullable private StateSaver.SavedState savedState;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Activity LifeCycle
|
// Activity LifeCycle
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -98,41 +115,28 @@ public final class MainVideoPlayer extends Activity {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
|
||||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||||
|
|
||||||
final Intent intent;
|
hideSystemUi();
|
||||||
if (savedInstanceState != null && savedInstanceState.getParcelable(PLAYER_STATE_INTENT) != null) {
|
|
||||||
intent = savedInstanceState.getParcelable(PLAYER_STATE_INTENT);
|
|
||||||
} else {
|
|
||||||
intent = getIntent();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showSystemUi();
|
|
||||||
setContentView(R.layout.activity_main_player);
|
setContentView(R.layout.activity_main_player);
|
||||||
playerImpl = new VideoPlayerImpl(this);
|
playerImpl = new VideoPlayerImpl(this);
|
||||||
playerImpl.setup(findViewById(android.R.id.content));
|
playerImpl.setup(findViewById(android.R.id.content));
|
||||||
|
|
||||||
|
if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) {
|
||||||
|
return; // We have saved states, stop here to restore it
|
||||||
|
}
|
||||||
|
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
if (intent != null) {
|
||||||
playerImpl.handleIntent(intent);
|
playerImpl.handleIntent(intent);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onRestoreInstanceState(bundle);
|
||||||
if (this.playerImpl == null) return;
|
savedState = StateSaver.tryToRestore(bundle, this);
|
||||||
|
|
||||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
|
||||||
getApplicationContext(),
|
|
||||||
this.getClass(),
|
|
||||||
this.playerImpl.getPlayQueue(),
|
|
||||||
this.playerImpl.getRepeatMode(),
|
|
||||||
this.playerImpl.getPlaybackSpeed(),
|
|
||||||
this.playerImpl.getPlaybackPitch(),
|
|
||||||
this.playerImpl.getPlaybackQuality()
|
|
||||||
);
|
|
||||||
outState.putParcelable(PLAYER_STATE_INTENT, intent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -142,6 +146,23 @@ public final class MainVideoPlayer extends Activity {
|
||||||
playerImpl.handleIntent(intent);
|
playerImpl.handleIntent(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||||
|
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
|
||||||
|
&& !playerImpl.isPlaying()) {
|
||||||
|
playerImpl.onPlay();
|
||||||
|
}
|
||||||
|
activityPaused = false;
|
||||||
|
|
||||||
|
if(globalScreenOrientationLocked()) {
|
||||||
|
boolean lastOrientationWasLandscape
|
||||||
|
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
|
||||||
|
setLandscape(lastOrientationWasLandscape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||||
|
@ -150,36 +171,35 @@ public final class MainVideoPlayer extends Activity {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onStop();
|
super.onConfigurationChanged(newConfig);
|
||||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
|
||||||
activityPaused = true;
|
|
||||||
|
|
||||||
if (playerImpl.getPlayer() != null) {
|
if (playerImpl.isSomePopupMenuVisible()) {
|
||||||
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
|
playerImpl.getQualityPopupMenu().dismiss();
|
||||||
playerImpl.setRecovery();
|
playerImpl.getPlaybackSpeedPopupMenu().dismiss();
|
||||||
playerImpl.destroyPlayer();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onPause() {
|
||||||
super.onResume();
|
super.onPause();
|
||||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
if (DEBUG) Log.d(TAG, "onPause() called");
|
||||||
if (activityPaused) {
|
|
||||||
playerImpl.initPlayer();
|
|
||||||
playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white);
|
|
||||||
|
|
||||||
playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying);
|
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
|
||||||
playerImpl.initPlayback(playerImpl.playQueue);
|
playerImpl.wasPlaying = playerImpl.isPlaying();
|
||||||
|
playerImpl.onPause();
|
||||||
|
}
|
||||||
|
activityPaused = true;
|
||||||
|
}
|
||||||
|
|
||||||
activityPaused = false;
|
@Override
|
||||||
}
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
if(globalScreenOrientationLocked()) {
|
super.onSaveInstanceState(outState);
|
||||||
boolean lastOrientationWasLandscape
|
if (playerImpl == null) return;
|
||||||
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
|
|
||||||
setLandScape(lastOrientationWasLandscape);
|
playerImpl.setRecovery();
|
||||||
}
|
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState,
|
||||||
|
outState, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -189,60 +209,105 @@ public final class MainVideoPlayer extends Activity {
|
||||||
if (playerImpl != null) playerImpl.destroy();
|
if (playerImpl != null) playerImpl.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
// State Saving
|
||||||
super.onConfigurationChanged(newConfig);
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
if (playerImpl.isSomePopupMenuVisible()) {
|
@Override
|
||||||
playerImpl.moreOptionsPopupMenu.dismiss();
|
public String generateSuffix() {
|
||||||
playerImpl.getQualityPopupMenu().dismiss();
|
return "." + UUID.randomUUID().toString() + ".player";
|
||||||
playerImpl.getPlaybackSpeedPopupMenu().dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(Queue<Object> objectsToSave) {
|
||||||
|
if (objectsToSave == null) return;
|
||||||
|
objectsToSave.add(playerImpl.getPlayQueue());
|
||||||
|
objectsToSave.add(playerImpl.getRepeatMode());
|
||||||
|
objectsToSave.add(playerImpl.getPlaybackSpeed());
|
||||||
|
objectsToSave.add(playerImpl.getPlaybackPitch());
|
||||||
|
objectsToSave.add(playerImpl.getPlaybackQuality());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||||
|
@NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll();
|
||||||
|
final int repeatMode = (int) savedObjects.poll();
|
||||||
|
final float playbackSpeed = (float) savedObjects.poll();
|
||||||
|
final float playbackPitch = (float) savedObjects.poll();
|
||||||
|
@NonNull final String playbackQuality = (String) savedObjects.poll();
|
||||||
|
|
||||||
|
playerImpl.setPlaybackQuality(playbackQuality);
|
||||||
|
playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
|
||||||
|
|
||||||
|
StateSaver.onDestroy(savedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Utils
|
// View
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two
|
||||||
|
* clicks to get rid of that invisible overlay. By showing the system UI on actions/events,
|
||||||
|
* that overlay is removed and the player view is put to the foreground.
|
||||||
|
*
|
||||||
|
* Post Kitkat, navbar and status bar can be pulled out by swiping the edge of
|
||||||
|
* screen, therefore, we can do nothing or hide the UI on actions/events.
|
||||||
|
* */
|
||||||
|
private void changeSystemUi() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
showSystemUi();
|
||||||
|
} else {
|
||||||
|
hideSystemUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showSystemUi() {
|
private void showSystemUi() {
|
||||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||||
if (playerImpl != null && playerImpl.queueVisible) return;
|
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||||
|
|
||||||
|
final int visibility;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
getWindow().getDecorView().setSystemUiVisibility(
|
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||||
);
|
} else {
|
||||||
} else getWindow().getDecorView().setSystemUiVisibility(0);
|
visibility = View.STATUS_BAR_VISIBLE;
|
||||||
|
}
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideSystemUi() {
|
private void hideSystemUi() {
|
||||||
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 16) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||||
|
}
|
||||||
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||||
}
|
}
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void toggleOrientation() {
|
private void toggleOrientation() {
|
||||||
setLandScape(!isLandScape());
|
setLandscape(!isLandscape());
|
||||||
defaultPreferences.edit()
|
defaultPreferences.edit()
|
||||||
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandScape())
|
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape())
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLandScape() {
|
private boolean isLandscape() {
|
||||||
return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels;
|
return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLandScape(boolean v) {
|
private void setLandscape(boolean v) {
|
||||||
setRequestedOrientation(v
|
setRequestedOrientation(v
|
||||||
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||||
|
@ -277,6 +342,15 @@ public final class MainVideoPlayer extends Activity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playback Parameters Listener
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||||
|
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||||
|
@ -301,8 +375,12 @@ public final class MainVideoPlayer extends Activity {
|
||||||
private boolean queueVisible;
|
private boolean queueVisible;
|
||||||
|
|
||||||
private ImageButton moreOptionsButton;
|
private ImageButton moreOptionsButton;
|
||||||
public int moreOptionsPopupMenuGroupId = 89;
|
private ImageButton toggleOrientationButton;
|
||||||
public PopupMenu moreOptionsPopupMenu;
|
private ImageButton switchPopupButton;
|
||||||
|
private ImageButton switchBackgroundButton;
|
||||||
|
|
||||||
|
private RelativeLayout windowRootLayout;
|
||||||
|
private View secondaryControls;
|
||||||
|
|
||||||
VideoPlayerImpl(final Context context) {
|
VideoPlayerImpl(final Context context) {
|
||||||
super("VideoPlayerImpl" + MainVideoPlayer.TAG, context);
|
super("VideoPlayerImpl" + MainVideoPlayer.TAG, context);
|
||||||
|
@ -322,9 +400,25 @@ public final class MainVideoPlayer extends Activity {
|
||||||
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
|
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
|
||||||
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
|
this.playPreviousButton = rootView.findViewById(R.id.playPreviousButton);
|
||||||
this.playNextButton = rootView.findViewById(R.id.playNextButton);
|
this.playNextButton = rootView.findViewById(R.id.playNextButton);
|
||||||
|
|
||||||
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
|
this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
|
||||||
this.moreOptionsPopupMenu = new PopupMenu(context, moreOptionsButton);
|
this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
|
||||||
buildMoreOptionsMenu();
|
this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
|
||||||
|
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
|
||||||
|
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
|
||||||
|
|
||||||
|
this.queueLayout = findViewById(R.id.playQueuePanel);
|
||||||
|
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
|
||||||
|
this.itemsList = findViewById(R.id.playQueue);
|
||||||
|
|
||||||
|
this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot);
|
||||||
|
// Prior to Kitkat, there is no way of setting translucent navbar programmatically.
|
||||||
|
// Thus, fit system windows is opted instead.
|
||||||
|
// See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
windowRootLayout.setFitsSystemWindows(false);
|
||||||
|
windowRootLayout.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
titleTextView.setSelected(true);
|
titleTextView.setSelected(true);
|
||||||
channelTextView.setSelected(true);
|
channelTextView.setSelected(true);
|
||||||
|
@ -332,6 +426,24 @@ public final class MainVideoPlayer extends Activity {
|
||||||
getRootView().setKeepScreenOn(true);
|
getRootView().setKeepScreenOn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSubtitleView(@NonNull SubtitleView view,
|
||||||
|
@NonNull String captionSizeKey) {
|
||||||
|
final float captionRatioInverse;
|
||||||
|
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
|
||||||
|
captionRatioInverse = 22f;
|
||||||
|
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
|
||||||
|
captionRatioInverse = 18f;
|
||||||
|
} else {
|
||||||
|
captionRatioInverse = 20f;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
|
||||||
|
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX,
|
||||||
|
(float) minimumLength / captionRatioInverse);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initListeners() {
|
public void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
@ -348,7 +460,11 @@ public final class MainVideoPlayer extends Activity {
|
||||||
playPauseButton.setOnClickListener(this);
|
playPauseButton.setOnClickListener(this);
|
||||||
playPreviousButton.setOnClickListener(this);
|
playPreviousButton.setOnClickListener(this);
|
||||||
playNextButton.setOnClickListener(this);
|
playNextButton.setOnClickListener(this);
|
||||||
|
|
||||||
moreOptionsButton.setOnClickListener(this);
|
moreOptionsButton.setOnClickListener(this);
|
||||||
|
toggleOrientationButton.setOnClickListener(this);
|
||||||
|
switchBackgroundButton.setOnClickListener(this);
|
||||||
|
switchPopupButton.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -361,31 +477,32 @@ public final class MainVideoPlayer extends Activity {
|
||||||
updatePlaybackButtons();
|
updatePlaybackButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Playback Listener
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdown() {
|
|
||||||
super.shutdown();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
|
||||||
super.sync(item, info);
|
|
||||||
titleTextView.setText(getVideoTitle());
|
|
||||||
channelTextView.setText(getUploaderName());
|
|
||||||
|
|
||||||
//playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onShuffleClicked() {
|
public void onShuffleClicked() {
|
||||||
super.onShuffleClicked();
|
super.onShuffleClicked();
|
||||||
updatePlaybackButtons();
|
updatePlaybackButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playback Listener
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||||
|
@Nullable final StreamInfo info,
|
||||||
|
final int newPlayQueueIndex,
|
||||||
|
final boolean hasPlayQueueItemChanged) {
|
||||||
|
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
|
||||||
|
|
||||||
|
titleTextView.setText(getVideoTitle());
|
||||||
|
channelTextView.setText(getUploaderName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackShutdown() {
|
||||||
|
super.onPlaybackShutdown();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Player Overrides
|
// Player Overrides
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
@ -445,7 +562,7 @@ public final class MainVideoPlayer extends Activity {
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
super.onClick(v);
|
super.onClick(v);
|
||||||
if (v.getId() == playPauseButton.getId()) {
|
if (v.getId() == playPauseButton.getId()) {
|
||||||
onVideoPlayPause();
|
onPlayPause();
|
||||||
|
|
||||||
} else if (v.getId() == playPreviousButton.getId()) {
|
} else if (v.getId() == playPreviousButton.getId()) {
|
||||||
onPlayPrevious();
|
onPlayPrevious();
|
||||||
|
@ -464,13 +581,23 @@ public final class MainVideoPlayer extends Activity {
|
||||||
return;
|
return;
|
||||||
} else if (v.getId() == moreOptionsButton.getId()) {
|
} else if (v.getId() == moreOptionsButton.getId()) {
|
||||||
onMoreOptionsClicked();
|
onMoreOptionsClicked();
|
||||||
|
|
||||||
|
} else if (v.getId() == toggleOrientationButton.getId()) {
|
||||||
|
onScreenRotationClicked();
|
||||||
|
|
||||||
|
} else if (v.getId() == switchPopupButton.getId()) {
|
||||||
|
onFullScreenButtonClicked();
|
||||||
|
|
||||||
|
} else if (v.getId() == switchBackgroundButton.getId()) {
|
||||||
|
onPlayBackgroundButtonClicked();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCurrentState() != STATE_COMPLETED) {
|
if (getCurrentState() != STATE_COMPLETED) {
|
||||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||||
animateView(getControlsRoot(), true, 300, 0, () -> {
|
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
|
||||||
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
|
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
|
||||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -484,22 +611,28 @@ public final class MainVideoPlayer extends Activity {
|
||||||
updatePlaybackButtons();
|
updatePlaybackButtons();
|
||||||
|
|
||||||
getControlsRoot().setVisibility(View.INVISIBLE);
|
getControlsRoot().setVisibility(View.INVISIBLE);
|
||||||
queueLayout.setVisibility(View.VISIBLE);
|
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
|
||||||
|
DEFAULT_CONTROLS_DURATION);
|
||||||
|
|
||||||
itemsList.scrollToPosition(playQueue.getIndex());
|
itemsList.scrollToPosition(playQueue.getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onQueueClosed() {
|
private void onQueueClosed() {
|
||||||
queueLayout.setVisibility(View.GONE);
|
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
|
||||||
|
DEFAULT_CONTROLS_DURATION);
|
||||||
queueVisible = false;
|
queueVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMoreOptionsClicked() {
|
private void onMoreOptionsClicked() {
|
||||||
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
|
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
|
||||||
|
|
||||||
moreOptionsPopupMenu.show();
|
final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
|
||||||
isSomePopupMenuVisible = true;
|
|
||||||
showControls(300);
|
animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
|
||||||
|
isMoreControlsVisible ? 0 : 180);
|
||||||
|
animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
|
||||||
|
DEFAULT_CONTROLS_DURATION);
|
||||||
|
showControls(DEFAULT_CONTROLS_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onScreenRotationClicked() {
|
private void onScreenRotationClicked() {
|
||||||
|
@ -508,18 +641,35 @@ public final class MainVideoPlayer extends Activity {
|
||||||
showControlsThenHide();
|
showControlsThenHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackSpeedClicked() {
|
||||||
|
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
|
||||||
|
.show(getSupportFragmentManager(), TAG);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
super.onStopTrackingTouch(seekBar);
|
super.onStopTrackingTouch(seekBar);
|
||||||
if (wasPlaying()) {
|
if (wasPlaying()) showControlsThenHide();
|
||||||
hideControls(100, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDismiss(PopupMenu menu) {
|
public void onDismiss(PopupMenu menu) {
|
||||||
super.onDismiss(menu);
|
super.onDismiss(menu);
|
||||||
if (isPlaying()) hideControls(300, 0);
|
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||||
|
hideSystemUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int nextResizeMode(int currentResizeMode) {
|
||||||
|
switch (currentResizeMode) {
|
||||||
|
case AspectRatioFrameLayout.RESIZE_MODE_FIT:
|
||||||
|
return AspectRatioFrameLayout.RESIZE_MODE_FILL;
|
||||||
|
case AspectRatioFrameLayout.RESIZE_MODE_FILL:
|
||||||
|
return AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||||
|
default:
|
||||||
|
return AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -565,7 +715,7 @@ public final class MainVideoPlayer extends Activity {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||||
animatePlayButtons(true, 200);
|
animatePlayButtons(true, 200);
|
||||||
});
|
});
|
||||||
showSystemUi();
|
|
||||||
getRootView().setKeepScreenOn(true);
|
getRootView().setKeepScreenOn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,7 +727,7 @@ public final class MainVideoPlayer extends Activity {
|
||||||
animatePlayButtons(true, 200);
|
animatePlayButtons(true, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
showSystemUi();
|
changeSystemUi();
|
||||||
getRootView().setKeepScreenOn(false);
|
getRootView().setKeepScreenOn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -591,10 +741,9 @@ public final class MainVideoPlayer extends Activity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCompleted() {
|
public void onCompleted() {
|
||||||
showSystemUi();
|
|
||||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
|
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||||
animatePlayButtons(true, 300);
|
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||||
});
|
});
|
||||||
|
|
||||||
getRootView().setKeepScreenOn(false);
|
getRootView().setKeepScreenOn(false);
|
||||||
|
@ -624,8 +773,9 @@ public final class MainVideoPlayer extends Activity {
|
||||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||||
getControlsVisibilityHandler().postDelayed(() ->
|
getControlsVisibilityHandler().postDelayed(() ->
|
||||||
animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi),
|
animateView(getControlsRoot(), false, duration, 0,
|
||||||
delay
|
MainVideoPlayer.this::hideSystemUi),
|
||||||
|
/*delayMillis=*/delay
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -637,48 +787,7 @@ public final class MainVideoPlayer extends Activity {
|
||||||
setShuffleButton(shuffleButton, playQueue.isShuffled());
|
setShuffleButton(shuffleButton, playQueue.isShuffled());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildMoreOptionsMenu() {
|
|
||||||
this.moreOptionsPopupMenu.getMenuInflater().inflate(R.menu.menu_videooptions,
|
|
||||||
moreOptionsPopupMenu.getMenu());
|
|
||||||
|
|
||||||
moreOptionsPopupMenu.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
switch (menuItem.getItemId()) {
|
|
||||||
case R.id.toggleOrientation:
|
|
||||||
onScreenRotationClicked();
|
|
||||||
break;
|
|
||||||
case R.id.switchPopup:
|
|
||||||
onFullScreenButtonClicked();
|
|
||||||
break;
|
|
||||||
case R.id.switchBackground:
|
|
||||||
onPlayBackgroundButtonClicked();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
PopupMenuIconHacker.setShowPopupIcon(moreOptionsPopupMenu);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix icon theme
|
|
||||||
if(ThemeHelper.isLightThemeSelected(MainVideoPlayer.this)) {
|
|
||||||
moreOptionsPopupMenu.getMenu()
|
|
||||||
.findItem(R.id.toggleOrientation)
|
|
||||||
.setIcon(R.drawable.ic_screen_rotation_black_24dp);
|
|
||||||
moreOptionsPopupMenu.getMenu()
|
|
||||||
.findItem(R.id.switchPopup)
|
|
||||||
.setIcon(R.drawable.ic_fullscreen_exit_black_24dp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildQueue() {
|
private void buildQueue() {
|
||||||
queueLayout = findViewById(R.id.playQueuePanel);
|
|
||||||
|
|
||||||
itemsListCloseButton = findViewById(R.id.playQueueClose);
|
|
||||||
|
|
||||||
itemsList = findViewById(R.id.playQueue);
|
|
||||||
itemsList.setAdapter(playQueueAdapter);
|
itemsList.setAdapter(playQueueAdapter);
|
||||||
itemsList.setClickable(true);
|
itemsList.setClickable(true);
|
||||||
itemsList.setLongClickable(true);
|
itemsList.setLongClickable(true);
|
||||||
|
@ -708,31 +817,11 @@ public final class MainVideoPlayer extends Activity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
return new PlayQueueItemTouchCallback() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
public void onMove(int sourceIndex, int targetIndex) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final int sourceIndex = source.getLayoutPosition();
|
|
||||||
final int targetIndex = target.getLayoutPosition();
|
|
||||||
playQueue.move(sourceIndex, targetIndex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLongPressDragEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isItemViewSwipeEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -807,14 +896,22 @@ public final class MainVideoPlayer extends Activity {
|
||||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||||
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
|
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
|
||||||
|
|
||||||
if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0);
|
if (playerImpl.isControlsVisible()) {
|
||||||
else {
|
playerImpl.hideControls(150, 0);
|
||||||
|
} else {
|
||||||
playerImpl.showControlsThenHide();
|
playerImpl.showControlsThenHide();
|
||||||
showSystemUi();
|
changeSystemUi();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(MotionEvent e) {
|
||||||
|
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||||
|
|
||||||
|
return super.onDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
|
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
|
||||||
|
|
||||||
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
|
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
|
||||||
|
@ -893,11 +990,15 @@ public final class MainVideoPlayer extends Activity {
|
||||||
eventsNum = 0;
|
eventsNum = 0;
|
||||||
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
||||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
|
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
|
||||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200);
|
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) {
|
||||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
animateView(playerImpl.getVolumeTextView(), false, 200, 200);
|
||||||
|
}
|
||||||
|
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) {
|
||||||
|
animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
||||||
|
}
|
||||||
|
|
||||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,11 @@ import android.widget.RemoteViews;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||||
|
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
@ -67,6 +70,9 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||||
|
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||||
|
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
|
||||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||||
|
|
||||||
|
@ -88,6 +94,8 @@ public final class PopupVideoPlayer extends Service {
|
||||||
private static final String POPUP_SAVED_X = "popup_saved_x";
|
private static final String POPUP_SAVED_X = "popup_saved_x";
|
||||||
private static final String POPUP_SAVED_Y = "popup_saved_y";
|
private static final String POPUP_SAVED_Y = "popup_saved_y";
|
||||||
|
|
||||||
|
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
|
||||||
|
|
||||||
private WindowManager windowManager;
|
private WindowManager windowManager;
|
||||||
private WindowManager.LayoutParams windowLayoutParams;
|
private WindowManager.LayoutParams windowLayoutParams;
|
||||||
private GestureDetector gestureDetector;
|
private GestureDetector gestureDetector;
|
||||||
|
@ -358,10 +366,12 @@ public final class PopupVideoPlayer extends Service {
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected class VideoPlayerImpl extends VideoPlayer {
|
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
|
||||||
private TextView resizingIndicator;
|
private TextView resizingIndicator;
|
||||||
private ImageButton fullScreenButton;
|
private ImageButton fullScreenButton;
|
||||||
|
|
||||||
|
private View extraOptionsView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleIntent(Intent intent) {
|
public void handleIntent(Intent intent) {
|
||||||
super.handleIntent(intent);
|
super.handleIntent(intent);
|
||||||
|
@ -380,6 +390,29 @@ public final class PopupVideoPlayer extends Service {
|
||||||
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
|
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
|
||||||
fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
|
fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
|
||||||
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
|
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
|
||||||
|
|
||||||
|
extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
|
||||||
|
rootView.addOnLayoutChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSubtitleView(@NonNull SubtitleView view,
|
||||||
|
@NonNull String captionSizeKey) {
|
||||||
|
float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION;
|
||||||
|
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
|
||||||
|
captionRatio *= 0.9;
|
||||||
|
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
|
||||||
|
captionRatio *= 1.1;
|
||||||
|
}
|
||||||
|
view.setFractionalTextSize(captionRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLayoutChange(final View view, int left, int top, int right, int bottom,
|
||||||
|
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||||
|
float widthDp = Math.abs(right - left) / getResources().getDisplayMetrics().density;
|
||||||
|
final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP ? View.VISIBLE : View.GONE;
|
||||||
|
extraOptionsView.setVisibility(visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -389,13 +422,15 @@ public final class PopupVideoPlayer extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||||
super.onThumbnailReceived(thumbnail);
|
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||||
if (thumbnail != null) {
|
if (loadedImage != null) {
|
||||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||||
notBuilder = createNotification();
|
notBuilder = createNotification();
|
||||||
|
|
||||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
if (notRemoteView != null) {
|
||||||
|
notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||||
|
}
|
||||||
|
|
||||||
updateNotification(-1);
|
updateNotification(-1);
|
||||||
}
|
}
|
||||||
|
@ -438,6 +473,15 @@ public final class PopupVideoPlayer extends Service {
|
||||||
if (isPlaying()) hideControls(500, 0);
|
if (isPlaying()) hideControls(500, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int nextResizeMode(int resizeMode) {
|
||||||
|
if (resizeMode == AspectRatioFrameLayout.RESIZE_MODE_FILL) {
|
||||||
|
return AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
|
} else {
|
||||||
|
return AspectRatioFrameLayout.RESIZE_MODE_FILL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
super.onStopTrackingTouch(seekBar);
|
super.onStopTrackingTouch(seekBar);
|
||||||
|
@ -494,7 +538,8 @@ public final class PopupVideoPlayer extends Service {
|
||||||
|
|
||||||
private void updatePlayback() {
|
private void updatePlayback() {
|
||||||
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
||||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
|
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
||||||
|
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,16 +578,17 @@ public final class PopupVideoPlayer extends Service {
|
||||||
// Playback Listener
|
// Playback Listener
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Override
|
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||||
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) {
|
@Nullable final StreamInfo info,
|
||||||
if (currentItem == item && currentInfo == info) return;
|
final int newPlayQueueIndex,
|
||||||
super.sync(item, info);
|
final boolean hasPlayQueueItemChanged) {
|
||||||
|
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
|
||||||
updateMetadata();
|
updateMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void onPlaybackShutdown() {
|
||||||
super.shutdown();
|
super.onPlaybackShutdown();
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -572,7 +618,7 @@ public final class PopupVideoPlayer extends Service {
|
||||||
onClose();
|
onClose();
|
||||||
break;
|
break;
|
||||||
case ACTION_PLAY_PAUSE:
|
case ACTION_PLAY_PAUSE:
|
||||||
onVideoPlayPause();
|
onPlayPause();
|
||||||
break;
|
break;
|
||||||
case ACTION_REPEAT:
|
case ACTION_REPEAT:
|
||||||
onRepeatClicked();
|
onRepeatClicked();
|
||||||
|
@ -607,6 +653,8 @@ public final class PopupVideoPlayer extends Service {
|
||||||
super.onPlaying();
|
super.onPlaying();
|
||||||
updateNotification(R.drawable.ic_pause_white);
|
updateNotification(R.drawable.ic_pause_white);
|
||||||
lockManager.acquireWifiAndCpu();
|
lockManager.acquireWifiAndCpu();
|
||||||
|
|
||||||
|
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -642,8 +690,8 @@ public final class PopupVideoPlayer extends Service {
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
/*package-private*/ void enableVideoRenderer(final boolean enable) {
|
/*package-private*/ void enableVideoRenderer(final boolean enable) {
|
||||||
final int videoRendererIndex = getVideoRendererIndex();
|
final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO);
|
||||||
if (trackSelector != null && videoRendererIndex != -1) {
|
if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) {
|
||||||
trackSelector.setRendererDisabled(videoRendererIndex, !enable);
|
trackSelector.setRendererDisabled(videoRendererIndex, !enable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -668,7 +716,7 @@ public final class PopupVideoPlayer extends Service {
|
||||||
public boolean onDoubleTap(MotionEvent e) {
|
public boolean onDoubleTap(MotionEvent e) {
|
||||||
if (DEBUG)
|
if (DEBUG)
|
||||||
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||||
if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false;
|
if (playerImpl == null || !playerImpl.isPlaying()) return false;
|
||||||
|
|
||||||
if (e.getX() > popupWidth / 2) {
|
if (e.getX() > popupWidth / 2) {
|
||||||
playerImpl.onFastForward();
|
playerImpl.onFastForward();
|
||||||
|
@ -683,7 +731,7 @@ public final class PopupVideoPlayer extends Service {
|
||||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||||
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
|
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
|
||||||
playerImpl.onVideoPlayPause();
|
playerImpl.onPlayPause();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -739,8 +787,8 @@ public final class PopupVideoPlayer extends Service {
|
||||||
private void onScrollEnd() {
|
private void onScrollEnd() {
|
||||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||||
if (playerImpl == null) return;
|
if (playerImpl == null) return;
|
||||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,10 +29,13 @@ 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.player.helper.PlaybackParameterDialog;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||||
|
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
|
||||||
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.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -41,7 +44,8 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
|
||||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||||
|
|
||||||
public abstract class ServicePlayerActivity extends AppCompatActivity
|
public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
|
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||||
|
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||||
|
|
||||||
private boolean serviceBound;
|
private boolean serviceBound;
|
||||||
private ServiceConnection serviceConnection;
|
private ServiceConnection serviceConnection;
|
||||||
|
@ -55,8 +59,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
||||||
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
|
|
||||||
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
|
|
||||||
|
|
||||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||||
|
|
||||||
|
@ -72,6 +74,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
private SeekBar progressSeekBar;
|
private SeekBar progressSeekBar;
|
||||||
private TextView progressCurrentTime;
|
private TextView progressCurrentTime;
|
||||||
private TextView progressEndTime;
|
private TextView progressEndTime;
|
||||||
|
private TextView progressLiveSync;
|
||||||
private TextView seekDisplay;
|
private TextView seekDisplay;
|
||||||
|
|
||||||
private ImageButton repeatButton;
|
private ImageButton repeatButton;
|
||||||
|
@ -82,9 +85,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
private TextView playbackSpeedButton;
|
private TextView playbackSpeedButton;
|
||||||
private PopupMenu playbackSpeedPopupMenu;
|
|
||||||
private TextView playbackPitchButton;
|
private TextView playbackPitchButton;
|
||||||
private PopupMenu playbackPitchPopupMenu;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Abstracts
|
// Abstracts
|
||||||
|
@ -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
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -202,6 +211,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
serviceBound = false;
|
serviceBound = false;
|
||||||
stopPlayerListener();
|
stopPlayerListener();
|
||||||
|
|
||||||
|
if (player != null && player.getPlayQueueAdapter() != null) {
|
||||||
|
player.getPlayQueueAdapter().unsetSelectedListener();
|
||||||
|
}
|
||||||
|
if (itemsList != null) itemsList.setAdapter(null);
|
||||||
|
if (itemTouchHelper != null) itemTouchHelper.attachToRecyclerView(null);
|
||||||
|
|
||||||
|
itemsList = null;
|
||||||
|
itemTouchHelper = null;
|
||||||
player = null;
|
player = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,9 +291,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||||
|
progressLiveSync = rootView.findViewById(R.id.live_sync);
|
||||||
seekDisplay = rootView.findViewById(R.id.seek_display);
|
seekDisplay = rootView.findViewById(R.id.seek_display);
|
||||||
|
|
||||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||||
|
progressLiveSync.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildControls() {
|
private void buildControls() {
|
||||||
|
@ -295,45 +315,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
shuffleButton.setOnClickListener(this);
|
shuffleButton.setOnClickListener(this);
|
||||||
playbackSpeedButton.setOnClickListener(this);
|
playbackSpeedButton.setOnClickListener(this);
|
||||||
playbackPitchButton.setOnClickListener(this);
|
playbackPitchButton.setOnClickListener(this);
|
||||||
|
|
||||||
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
|
|
||||||
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
|
|
||||||
buildPlaybackSpeedMenu();
|
|
||||||
buildPlaybackPitchMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildPlaybackSpeedMenu() {
|
|
||||||
if (playbackSpeedPopupMenu == null) return;
|
|
||||||
|
|
||||||
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
|
|
||||||
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
|
|
||||||
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
|
|
||||||
final String formattedSpeed = formatSpeed(playbackSpeed);
|
|
||||||
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
|
|
||||||
item.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
if (player == null) return false;
|
|
||||||
|
|
||||||
player.setPlaybackSpeed(playbackSpeed);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildPlaybackPitchMenu() {
|
|
||||||
if (playbackPitchPopupMenu == null) return;
|
|
||||||
|
|
||||||
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
|
|
||||||
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
|
|
||||||
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
|
|
||||||
final String formattedPitch = formatPitch(playbackPitch);
|
|
||||||
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
|
|
||||||
item.setOnMenuItemClickListener(menuItem -> {
|
|
||||||
if (player == null) return false;
|
|
||||||
|
|
||||||
player.setPlaybackPitch(playbackPitch);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||||
|
@ -374,31 +355,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
return new PlayQueueItemTouchCallback() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
public void onMove(int sourceIndex, int targetIndex) {
|
||||||
if (source.getItemViewType() != target.getItemViewType()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int sourceIndex = source.getLayoutPosition();
|
|
||||||
final int targetIndex = target.getLayoutPosition();
|
|
||||||
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
|
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLongPressDragEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isItemViewSwipeEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,7 +424,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
player.onPlayPrevious();
|
player.onPlayPrevious();
|
||||||
|
|
||||||
} else if (view.getId() == playPauseButton.getId()) {
|
} else if (view.getId() == playPauseButton.getId()) {
|
||||||
player.onVideoPlayPause();
|
player.onPlayPause();
|
||||||
|
|
||||||
} else if (view.getId() == forwardButton.getId()) {
|
} else if (view.getId() == forwardButton.getId()) {
|
||||||
player.onPlayNext();
|
player.onPlayNext();
|
||||||
|
@ -472,17 +433,35 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
player.onShuffleClicked();
|
player.onShuffleClicked();
|
||||||
|
|
||||||
} else if (view.getId() == playbackSpeedButton.getId()) {
|
} else if (view.getId() == playbackSpeedButton.getId()) {
|
||||||
playbackSpeedPopupMenu.show();
|
openPlaybackParameterDialog();
|
||||||
|
|
||||||
} else if (view.getId() == playbackPitchButton.getId()) {
|
} else if (view.getId() == playbackPitchButton.getId()) {
|
||||||
playbackPitchPopupMenu.show();
|
openPlaybackParameterDialog();
|
||||||
|
|
||||||
} else if (view.getId() == metadata.getId()) {
|
} else if (view.getId() == metadata.getId()) {
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
|
|
||||||
|
} else if (view.getId() == progressLiveSync.getId()) {
|
||||||
|
player.seekToDefault();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Playback Parameters
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void openPlaybackParameterDialog() {
|
||||||
|
if (player == null) return;
|
||||||
|
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
||||||
|
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||||
|
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Seekbar Listener
|
// Seekbar Listener
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -504,7 +483,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
if (player != null) player.seekTo(seekBar.getProgress());
|
||||||
seekDisplay.setVisibility(View.GONE);
|
seekDisplay.setVisibility(View.GONE);
|
||||||
seeking = false;
|
seeking = false;
|
||||||
}
|
}
|
||||||
|
@ -534,13 +513,30 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||||
progressSeekBar.setProgress(currentProgress);
|
progressSeekBar.setProgress(currentProgress);
|
||||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (player != null) {
|
||||||
|
progressLiveSync.setClickable(!player.isLiveEdge());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadataUpdate(StreamInfo info) {
|
public void onMetadataUpdate(StreamInfo info) {
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
metadataTitle.setText(info.getName());
|
metadataTitle.setText(info.getName());
|
||||||
metadataArtist.setText(info.uploader_name);
|
metadataArtist.setText(info.getUploaderName());
|
||||||
|
|
||||||
|
progressEndTime.setVisibility(View.GONE);
|
||||||
|
progressLiveSync.setVisibility(View.GONE);
|
||||||
|
switch (info.getStreamType()) {
|
||||||
|
case LIVE_STREAM:
|
||||||
|
case AUDIO_LIVE_STREAM:
|
||||||
|
progressLiveSync.setVisibility(View.VISIBLE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
progressEndTime.setVisibility(View.VISIBLE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
scrollToSelected();
|
scrollToSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue