mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 07:13:00 +00:00 
			
		
		
		
	Added basic channel subscription and feed pages (#620)
Added basic channel subscription and feed pages - Room Persistence for sqlite support. - RxJava2 for reactive async support. - Stetho for database inspection support. - Enabled Multidex for debug build.
This commit is contained in:
		| @@ -18,6 +18,12 @@ android { | |||||||
|             minifyEnabled false |             minifyEnabled false | ||||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' |             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||||
|         } |         } | ||||||
|  |         debug { | ||||||
|  |             multiDexEnabled true | ||||||
|  |  | ||||||
|  |             debuggable true | ||||||
|  |             applicationIdSuffix ".debug" | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     lintOptions { |     lintOptions { | ||||||
| @@ -58,4 +64,16 @@ dependencies { | |||||||
|     compile 'com.github.nirhart:parallaxscroll:1.0' |     compile 'com.github.nirhart:parallaxscroll:1.0' | ||||||
|     compile 'com.nononsenseapps:filepicker:3.0.0' |     compile 'com.nononsenseapps:filepicker:3.0.0' | ||||||
|     compile 'com.google.android.exoplayer:exoplayer:r2.4.2' |     compile 'com.google.android.exoplayer:exoplayer:r2.4.2' | ||||||
|  |  | ||||||
|  |     debugCompile 'com.facebook.stetho:stetho:1.5.0' | ||||||
|  |     debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0' | ||||||
|  |     debugCompile 'com.android.support:multidex:1.0.1' | ||||||
|  |  | ||||||
|  |     compile "android.arch.persistence.room:runtime:1.0.0-alpha8" | ||||||
|  |     annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha8" | ||||||
|  |  | ||||||
|  |     compile "io.reactivex.rxjava2:rxjava:2.1.2" | ||||||
|  |     compile "io.reactivex.rxjava2:rxandroid:2.0.1" | ||||||
|  |     compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' | ||||||
|  |     compile "android.arch.persistence.room:rxjava2:1.0.0-alpha8" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								app/src/debug/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/debug/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <manifest | ||||||
|  |     package="org.schabi.newpipe" | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|  |  | ||||||
|  |     <uses-permission android:name="android.permission.INTERNET"/> | ||||||
|  |     <uses-permission android:name="android.permission.WAKE_LOCK"/> | ||||||
|  |     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> | ||||||
|  |     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> | ||||||
|  |     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> | ||||||
|  |  | ||||||
|  |     <application | ||||||
|  |         tools:replace="android:name" | ||||||
|  |         android:name=".DebugApp"/> | ||||||
|  |  | ||||||
|  | </manifest> | ||||||
							
								
								
									
										63
									
								
								app/src/debug/java/org/schabi/newpipe/DebugApp.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								app/src/debug/java/org/schabi/newpipe/DebugApp.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | package org.schabi.newpipe; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
|  | import android.support.multidex.MultiDex; | ||||||
|  |  | ||||||
|  | import com.facebook.stetho.Stetho; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> | ||||||
|  |  * App.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 DebugApp extends App { | ||||||
|  |     private static final String TAG = DebugApp.class.toString(); | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void attachBaseContext(Context base) { | ||||||
|  |         super.attachBaseContext(base); | ||||||
|  |         MultiDex.install(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate() { | ||||||
|  |         super.onCreate(); | ||||||
|  |  | ||||||
|  |         initStetho(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void initStetho() { | ||||||
|  |         // Create an InitializerBuilder | ||||||
|  |         Stetho.InitializerBuilder initializerBuilder = | ||||||
|  |                 Stetho.newInitializerBuilder(this); | ||||||
|  |  | ||||||
|  |         // Enable Chrome DevTools | ||||||
|  |         initializerBuilder.enableWebKitInspector( | ||||||
|  |                 Stetho.defaultInspectorModulesProvider(this) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // Enable command line interface | ||||||
|  |         initializerBuilder.enableDumpapp( | ||||||
|  |                 Stetho.defaultDumperPluginsProvider(getApplicationContext()) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // Use the InitializerBuilder to generate an Initializer | ||||||
|  |         Stetho.Initializer initializer = initializerBuilder.build(); | ||||||
|  |  | ||||||
|  |         // Initialize Stetho with the Initializer | ||||||
|  |         Stetho.initialize(initializer); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ package org.schabi.newpipe; | |||||||
| import android.app.Application; | import android.app.Application; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  |  | ||||||
|  | import com.facebook.stetho.Stetho; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoader; | import com.nostra13.universalimageloader.core.ImageLoader; | ||||||
| import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; | import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; | ||||||
|  |  | ||||||
| @@ -64,6 +65,8 @@ public class App extends Application { | |||||||
|                             "Could not initialize ACRA crash report", R.string.app_ui_crash)); |                             "Could not initialize ACRA crash report", R.string.app_ui_crash)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         NewPipeDatabase.getInstance( getApplicationContext() ); | ||||||
|  |  | ||||||
|         //init NewPipe |         //init NewPipe | ||||||
|         NewPipe.init(Downloader.getInstance()); |         NewPipe.init(Downloader.getInstance()); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,7 +24,11 @@ import android.content.Intent; | |||||||
| import android.content.SharedPreferences; | import android.content.SharedPreferences; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.preference.PreferenceManager; | import android.preference.PreferenceManager; | ||||||
|  | import android.support.design.widget.TabLayout; | ||||||
| import android.support.v4.app.Fragment; | import android.support.v4.app.Fragment; | ||||||
|  | import android.support.v4.app.FragmentManager; | ||||||
|  | import android.support.v4.app.FragmentStatePagerAdapter; | ||||||
|  | import android.support.v4.view.ViewPager; | ||||||
| import android.support.v7.app.ActionBar; | import android.support.v7.app.ActionBar; | ||||||
| import android.support.v7.app.AppCompatActivity; | import android.support.v7.app.AppCompatActivity; | ||||||
| import android.support.v7.widget.Toolbar; | import android.support.v7.widget.Toolbar; | ||||||
| @@ -36,6 +40,9 @@ import android.view.View; | |||||||
|  |  | ||||||
| import org.schabi.newpipe.download.DownloadActivity; | import org.schabi.newpipe.download.DownloadActivity; | ||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
|  | import org.schabi.newpipe.fragments.FeedFragment; | ||||||
|  | import org.schabi.newpipe.fragments.MainFragment; | ||||||
|  | import org.schabi.newpipe.fragments.SubscriptionFragment; | ||||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||||
| import org.schabi.newpipe.fragments.search.SearchFragment; | import org.schabi.newpipe.fragments.search.SearchFragment; | ||||||
| import org.schabi.newpipe.settings.SettingsActivity; | import org.schabi.newpipe.settings.SettingsActivity; | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | package org.schabi.newpipe; | ||||||
|  |  | ||||||
|  | import android.arch.persistence.room.Room; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.support.annotation.NonNull; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.database.AppDatabase; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; | ||||||
|  |  | ||||||
|  | public class NewPipeDatabase { | ||||||
|  |  | ||||||
|  |     private static AppDatabase sInstance; | ||||||
|  |  | ||||||
|  |     // For Singleton instantiation | ||||||
|  |     private static final Object LOCK = new Object(); | ||||||
|  |  | ||||||
|  |     @NonNull | ||||||
|  |     public synchronized static AppDatabase getInstance(Context context) { | ||||||
|  |         if (sInstance == null) { | ||||||
|  |             synchronized (LOCK) { | ||||||
|  |                 if (sInstance == null) { | ||||||
|  |  | ||||||
|  |                     sInstance = Room.databaseBuilder( | ||||||
|  |                             context.getApplicationContext(), | ||||||
|  |                             AppDatabase.class, | ||||||
|  |                             DATABASE_NAME | ||||||
|  |                     ).build(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return sInstance; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | package org.schabi.newpipe.database; | ||||||
|  |  | ||||||
|  | import android.arch.persistence.room.Database; | ||||||
|  | import android.arch.persistence.room.RoomDatabase; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionDAO; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  |  | ||||||
|  | @Database(entities = {SubscriptionEntity.class}, version = 1, exportSchema = false) | ||||||
|  | public abstract class AppDatabase extends RoomDatabase{ | ||||||
|  |  | ||||||
|  |     public static final String DATABASE_NAME = "newpipe.db"; | ||||||
|  |  | ||||||
|  |     public abstract SubscriptionDAO subscriptionDAO(); | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | package org.schabi.newpipe.database; | ||||||
|  |  | ||||||
|  | import android.arch.persistence.room.Dao; | ||||||
|  | import android.arch.persistence.room.Delete; | ||||||
|  | import android.arch.persistence.room.Insert; | ||||||
|  | import android.arch.persistence.room.OnConflictStrategy; | ||||||
|  | import android.arch.persistence.room.Update; | ||||||
|  |  | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.Flowable; | ||||||
|  |  | ||||||
|  | @Dao | ||||||
|  | public interface BasicDAO<Entity> { | ||||||
|  |     /* Inserts */ | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.FAIL) | ||||||
|  |     long insert(final Entity entity); | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.FAIL) | ||||||
|  |     List<Long> insertAll(final Entity... entities); | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.FAIL) | ||||||
|  |     List<Long> insertAll(final Collection<Entity> entities); | ||||||
|  |  | ||||||
|  |     @Insert(onConflict = OnConflictStrategy.REPLACE) | ||||||
|  |     long upsert(final Entity entity); | ||||||
|  |  | ||||||
|  |     /* Searches */ | ||||||
|  |     Flowable<List<Entity>> findAll(); | ||||||
|  |  | ||||||
|  |     Flowable<List<Entity>> listByService(int serviceId); | ||||||
|  |  | ||||||
|  |     /* Deletes */ | ||||||
|  |     @Delete | ||||||
|  |     int delete(final Entity entity); | ||||||
|  |  | ||||||
|  |     @Delete | ||||||
|  |     int delete(final Collection<Entity> entities); | ||||||
|  |  | ||||||
|  |     /* Updates */ | ||||||
|  |     @Update | ||||||
|  |     int update(final Entity entity); | ||||||
|  |  | ||||||
|  |     @Update | ||||||
|  |     int update(final Collection<Entity> entities); | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package org.schabi.newpipe.database.subscription; | ||||||
|  |  | ||||||
|  | import android.arch.persistence.room.Dao; | ||||||
|  | import android.arch.persistence.room.Query; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.database.BasicDAO; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import io.reactivex.Flowable; | ||||||
|  |  | ||||||
|  | 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_URL; | ||||||
|  |  | ||||||
|  | @Dao | ||||||
|  | public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> { | ||||||
|  |     @Override | ||||||
|  |     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE) | ||||||
|  |     Flowable<List<SubscriptionEntity>> findAll(); | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId") | ||||||
|  |     Flowable<List<SubscriptionEntity>> listByService(int serviceId); | ||||||
|  |  | ||||||
|  |     @Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + | ||||||
|  |             SUBSCRIPTION_URL + " LIKE :url AND " + | ||||||
|  |             SUBSCRIPTION_SERVICE_ID + " = :serviceId") | ||||||
|  |     Flowable<List<SubscriptionEntity>> findAll(int serviceId, String url); | ||||||
|  | } | ||||||
| @@ -0,0 +1,113 @@ | |||||||
|  | package org.schabi.newpipe.database.subscription; | ||||||
|  |  | ||||||
|  | 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 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_URL; | ||||||
|  |  | ||||||
|  | @Entity(tableName = SUBSCRIPTION_TABLE, | ||||||
|  |         indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)}) | ||||||
|  | public class SubscriptionEntity { | ||||||
|  |  | ||||||
|  |     final static String SUBSCRIPTION_TABLE              = "subscriptions"; | ||||||
|  |     final static String SUBSCRIPTION_SERVICE_ID         = "service_id"; | ||||||
|  |     final static String SUBSCRIPTION_URL                = "url"; | ||||||
|  |     final static String SUBSCRIPTION_TITLE              = "title"; | ||||||
|  |     final static String SUBSCRIPTION_THUMBNAIL_URL      = "thumbnail_url"; | ||||||
|  |     final static String SUBSCRIPTION_SUBSCRIBER_COUNT   = "subscriber_count"; | ||||||
|  |     final static String SUBSCRIPTION_DESCRIPTION        = "description"; | ||||||
|  |  | ||||||
|  |     @PrimaryKey(autoGenerate = true) | ||||||
|  |     private long uid = 0; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID) | ||||||
|  |     private int serviceId = -1; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_URL) | ||||||
|  |     private String url; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_TITLE) | ||||||
|  |     private String title; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_THUMBNAIL_URL) | ||||||
|  |     private String thumbnailUrl; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) | ||||||
|  |     private Long subscriberCount; | ||||||
|  |  | ||||||
|  |     @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) | ||||||
|  |     private String description; | ||||||
|  |  | ||||||
|  |     public long getUid() { | ||||||
|  |         return uid; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /* Keep this package-private since UID should always be auto generated by Room impl */ | ||||||
|  |     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 String getThumbnailUrl() { | ||||||
|  |         return thumbnailUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setThumbnailUrl(String thumbnailUrl) { | ||||||
|  |         this.thumbnailUrl = thumbnailUrl; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Long getSubscriberCount() { | ||||||
|  |         return subscriberCount; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setSubscriberCount(Long subscriberCount) { | ||||||
|  |         this.subscriberCount = subscriberCount; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getDescription() { | ||||||
|  |         return description; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setDescription(String description) { | ||||||
|  |         this.description = description; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Ignore | ||||||
|  |     public void setData(final String title, | ||||||
|  |                         final String thumbnailUrl, | ||||||
|  |                         final String description, | ||||||
|  |                         final Long subscriberCount) { | ||||||
|  |         this.setTitle(title); | ||||||
|  |         this.setThumbnailUrl(thumbnailUrl); | ||||||
|  |         this.setDescription(description); | ||||||
|  |         this.setSubscriberCount(subscriberCount); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | package org.schabi.newpipe.fragments; | ||||||
|  |  | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  |  | ||||||
|  | public class BlankFragment extends BaseFragment { | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         return inflater.inflate(R.layout.fragment_blank, container, false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void reloadContent() { | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										495
									
								
								app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										495
									
								
								app/src/main/java/org/schabi/newpipe/fragments/FeedFragment.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,495 @@ | |||||||
|  | package org.schabi.newpipe.fragments; | ||||||
|  |  | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Parcelable; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | 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.LayoutInflater; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  |  | ||||||
|  | import com.jakewharton.rxbinding2.view.RxView; | ||||||
|  |  | ||||||
|  | import org.reactivestreams.Subscriber; | ||||||
|  | import org.reactivestreams.Subscription; | ||||||
|  | import org.schabi.newpipe.MainActivity; | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  | import org.schabi.newpipe.extractor.InfoItem; | ||||||
|  | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
|  | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  | import org.schabi.newpipe.info_list.InfoListAdapter; | ||||||
|  | import org.schabi.newpipe.report.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  |  | ||||||
|  | import io.reactivex.Flowable; | ||||||
|  | import io.reactivex.MaybeObserver; | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | import io.reactivex.annotations.NonNull; | ||||||
|  | import io.reactivex.disposables.Disposable; | ||||||
|  | import io.reactivex.functions.Consumer; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; | ||||||
|  | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
|  |  | ||||||
|  | public class FeedFragment extends BaseFragment { | ||||||
|  |     private static final String VIEW_STATE_KEY = "view_state_key"; | ||||||
|  |     private static final String INFO_ITEMS_KEY = "info_items_key"; | ||||||
|  |  | ||||||
|  |     private static final int FEED_LOAD_SIZE = 4; | ||||||
|  |     private static final int LOAD_ITEM_DEBOUNCE_INTERVAL = 500; | ||||||
|  |  | ||||||
|  |     private final String TAG = "FeedFragment@" + Integer.toHexString(hashCode()); | ||||||
|  |  | ||||||
|  |     private View inflatedView; | ||||||
|  |     private View emptyPanel; | ||||||
|  |     private View loadItemFooter; | ||||||
|  |  | ||||||
|  |     private InfoListAdapter infoListAdapter; | ||||||
|  |     private RecyclerView resultRecyclerView; | ||||||
|  |  | ||||||
|  |     private Parcelable viewState; | ||||||
|  |     private AtomicBoolean retainFeedItems; | ||||||
|  |  | ||||||
|  |     private SubscriptionService subscriptionService; | ||||||
|  |  | ||||||
|  |     private Disposable loadItemObserver; | ||||||
|  |     private Disposable subscriptionObserver; | ||||||
|  |     private Subscription feedSubscriber; | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment LifeCycle | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |  | ||||||
|  |         subscriptionService = SubscriptionService.getInstance(getContext()); | ||||||
|  |  | ||||||
|  |         retainFeedItems = new AtomicBoolean(false); | ||||||
|  |  | ||||||
|  |         if (infoListAdapter == null) { | ||||||
|  |             infoListAdapter = new InfoListAdapter(getActivity()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (savedInstanceState != null) { | ||||||
|  |             // Get recycler view state | ||||||
|  |             viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); | ||||||
|  |  | ||||||
|  |             // Deserialize and get recycler adapter list | ||||||
|  |             final Object[] serializedInfoItems = (Object[]) savedInstanceState.getSerializable(INFO_ITEMS_KEY); | ||||||
|  |             if (serializedInfoItems != null) { | ||||||
|  |                 final InfoItem[] infoItems = Arrays.copyOf( | ||||||
|  |                         serializedInfoItems, | ||||||
|  |                         serializedInfoItems.length, | ||||||
|  |                         InfoItem[].class | ||||||
|  |                 ); | ||||||
|  |                 final List<InfoItem> feedInfos = Arrays.asList(infoItems); | ||||||
|  |                 infoListAdapter.addInfoItemList( feedInfos ); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Already displayed feed items survive configuration changes | ||||||
|  |             retainFeedItems.set(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         if (inflatedView == null) { | ||||||
|  |             inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); | ||||||
|  |         } | ||||||
|  |         return inflatedView; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSaveInstanceState(Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |  | ||||||
|  |         if (resultRecyclerView != null) { | ||||||
|  |             outState.putParcelable( | ||||||
|  |                     VIEW_STATE_KEY, | ||||||
|  |                     resultRecyclerView.getLayoutManager().onSaveInstanceState() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (infoListAdapter != null) { | ||||||
|  |             outState.putSerializable(INFO_ITEMS_KEY, infoListAdapter.getItemsList().toArray()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|  |         // Do not monitor for updates when user is not viewing the feed fragment. | ||||||
|  |         // This is a waste of bandwidth. | ||||||
|  |         if (loadItemObserver != null) loadItemObserver.dispose(); | ||||||
|  |         if (subscriptionObserver != null) subscriptionObserver.dispose(); | ||||||
|  |         if (feedSubscriber != null) feedSubscriber.cancel(); | ||||||
|  |  | ||||||
|  |         loadItemObserver = null; | ||||||
|  |         subscriptionObserver = null; | ||||||
|  |         feedSubscriber = null; | ||||||
|  |  | ||||||
|  |         loadItemFooter = null; | ||||||
|  |  | ||||||
|  |         // Retain the already displayed items for backstack pops | ||||||
|  |         retainFeedItems.set(true); | ||||||
|  |  | ||||||
|  |         super.onDestroyView(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         subscriptionService = null; | ||||||
|  |  | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment Views | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); | ||||||
|  |         super.onCreateOptionsMenu(menu, inflater); | ||||||
|  |  | ||||||
|  |         ActionBar supportActionBar = activity.getSupportActionBar(); | ||||||
|  |         if (supportActionBar != null) { | ||||||
|  |             supportActionBar.setDisplayShowTitleEnabled(true); | ||||||
|  |             supportActionBar.setDisplayHomeAsUpEnabled(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private RecyclerView.OnScrollListener getOnScrollListener() { | ||||||
|  |         return new RecyclerView.OnScrollListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onScrollStateChanged(RecyclerView recyclerView, int newState) { | ||||||
|  |                 super.onScrollStateChanged(recyclerView, newState); | ||||||
|  |                 if (newState == RecyclerView.SCROLL_STATE_IDLE) { | ||||||
|  |                     viewState = recyclerView.getLayoutManager().onSaveInstanceState(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||||
|  |         super.initViews(rootView, savedInstanceState); | ||||||
|  |  | ||||||
|  |         if (infoListAdapter == null) return; | ||||||
|  |  | ||||||
|  |         animateView(errorPanel, false, 200); | ||||||
|  |         animateView(loadingProgressBar, true, 200); | ||||||
|  |  | ||||||
|  |         emptyPanel = rootView.findViewById(R.id.empty_panel); | ||||||
|  |  | ||||||
|  |         resultRecyclerView = rootView.findViewById(R.id.result_list_view); | ||||||
|  |         resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); | ||||||
|  |  | ||||||
|  |         loadItemFooter = activity.getLayoutInflater().inflate(R.layout.load_item_footer, resultRecyclerView, false); | ||||||
|  |         infoListAdapter.setFooter(loadItemFooter); | ||||||
|  |         infoListAdapter.showFooter(false); | ||||||
|  |         infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { | ||||||
|  |             @Override | ||||||
|  |             public void selected(int serviceId, String url, String title) { | ||||||
|  |                 NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         resultRecyclerView.setAdapter(infoListAdapter); | ||||||
|  |         resultRecyclerView.addOnScrollListener(getOnScrollListener()); | ||||||
|  |  | ||||||
|  |         if (viewState != null) { | ||||||
|  |             resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); | ||||||
|  |             viewState = null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(R.string.fragment_whats_new); | ||||||
|  |  | ||||||
|  |         populateFeed(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void resetFragment() { | ||||||
|  |         if (subscriptionObserver != null) subscriptionObserver.dispose(); | ||||||
|  |         if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void reloadContent() { | ||||||
|  |         resetFragment(); | ||||||
|  |         populateFeed(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void setErrorMessage(String message, boolean showRetryButton) { | ||||||
|  |         super.setErrorMessage(message, showRetryButton); | ||||||
|  |  | ||||||
|  |         resetFragment(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Changes the state of the load item footer. | ||||||
|  |      * | ||||||
|  |      * If the current state of the feed is loaded, this displays the load item button and | ||||||
|  |      * starts its reactor. | ||||||
|  |      * | ||||||
|  |      * Otherwise, show a spinner in place of the loader button. */ | ||||||
|  |     private void setLoader(final boolean isLoaded) { | ||||||
|  |         if (loadItemFooter == null) return; | ||||||
|  |  | ||||||
|  |         if (loadItemObserver != null) loadItemObserver.dispose(); | ||||||
|  |  | ||||||
|  |         if (isLoaded) { | ||||||
|  |             loadItemObserver = getLoadItemObserver(loadItemFooter); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         loadItemFooter.findViewById(R.id.paginate_progress_bar).setVisibility(isLoaded ? View.GONE : View.VISIBLE); | ||||||
|  |         loadItemFooter.findViewById(R.id.load_more_text).setVisibility(isLoaded ? View.VISIBLE : View.GONE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Feeds Loader | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Responsible for reacting to subscription database updates and displaying feeds. | ||||||
|  |      * | ||||||
|  |      * Upon each update, the feed info list is cleared unless the fragment is | ||||||
|  |      * recently recovered from a configuration change or backstack. | ||||||
|  |      * | ||||||
|  |      * All existing and pending feed requests are dropped. | ||||||
|  |      * | ||||||
|  |      * The newly received list of subscriptions is then transformed into a | ||||||
|  |      * flowable, reacting to pulling requests. | ||||||
|  |      * | ||||||
|  |      * Pulled requests are transformed first into ChannelInfo, then Stream Info items and | ||||||
|  |      * displayed on the feed fragment. | ||||||
|  |      **/ | ||||||
|  |     private void populateFeed() { | ||||||
|  |         final Consumer<List<SubscriptionEntity>> consumer = new Consumer<List<SubscriptionEntity>>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||||
|  |                 animateView(loadingProgressBar, false, 200); | ||||||
|  |  | ||||||
|  |                 if (subscriptionEntities.isEmpty()) { | ||||||
|  |                     infoListAdapter.clearStreamItemList(); | ||||||
|  |                     emptyPanel.setVisibility(View.VISIBLE); | ||||||
|  |                 } else { | ||||||
|  |                     emptyPanel.setVisibility(View.INVISIBLE); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // show progress bar on receiving a non-empty updated list of subscriptions | ||||||
|  |                 if (!retainFeedItems.get() && !subscriptionEntities.isEmpty()) { | ||||||
|  |                     infoListAdapter.clearStreamItemList(); | ||||||
|  |                     animateView(loadingProgressBar, true, 200); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 retainFeedItems.set(false); | ||||||
|  |                 Flowable.fromIterable(subscriptionEntities) | ||||||
|  |                         .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                         .subscribe(getSubscriptionObserver()); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(@NonNull Throwable exception) throws Exception { | ||||||
|  |                 onRxError(exception, "Subscription Database Reactor"); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (subscriptionObserver != null) subscriptionObserver.dispose(); | ||||||
|  |         subscriptionObserver = subscriptionService.getSubscription() | ||||||
|  |                 .onErrorReturnItem(Collections.<SubscriptionEntity>emptyList()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(consumer, onError); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Responsible for reacting to user pulling request and starting a request for new feed stream. | ||||||
|  |      * | ||||||
|  |      * On initialization, it automatically requests the amount of feed needed to display | ||||||
|  |      * a minimum amount required (FEED_LOAD_SIZE). | ||||||
|  |      * | ||||||
|  |      * Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo | ||||||
|  |      * containing the feed streams. | ||||||
|  |      **/ | ||||||
|  |     private Subscriber<SubscriptionEntity> getSubscriptionObserver() { | ||||||
|  |         return new Subscriber<SubscriptionEntity>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSubscribe(Subscription s) { | ||||||
|  |                 if (feedSubscriber != null) feedSubscriber.cancel(); | ||||||
|  |                 feedSubscriber = s; | ||||||
|  |  | ||||||
|  |                 final int requestSize = FEED_LOAD_SIZE - infoListAdapter.getItemsList().size(); | ||||||
|  |                 if (requestSize > 0) { | ||||||
|  |                     requestFeed(requestSize); | ||||||
|  |                 } else { | ||||||
|  |                     setLoader(true); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 animateView(loadingProgressBar, false, 200); | ||||||
|  |                 // Footer spinner persists until subscription list is exhausted. | ||||||
|  |                 infoListAdapter.showFooter(true); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onNext(SubscriptionEntity subscriptionEntity) { | ||||||
|  |                 setLoader(false); | ||||||
|  |  | ||||||
|  |                 subscriptionService.getChannelInfo(subscriptionEntity) | ||||||
|  |                         .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                         .onErrorComplete() | ||||||
|  |                         .subscribe(getChannelInfoObserver()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onError(Throwable exception) { | ||||||
|  |                 onRxError(exception, "Feed Pull Reactor"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onComplete() { | ||||||
|  |                 infoListAdapter.showFooter(false); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * On each request, a subscription item from the updated table is transformed | ||||||
|  |      * into a ChannelInfo, containing the latest streams from the channel. | ||||||
|  |      * | ||||||
|  |      * Currently, the feed uses the first into from the list of streams. | ||||||
|  |      * | ||||||
|  |      * If chosen feed already displayed, then we request another feed from another | ||||||
|  |      * subscription, until the subscription table runs out of new items. | ||||||
|  |      * | ||||||
|  |      * This Observer is self-contained and will dispose itself when complete. However, this | ||||||
|  |      * does not obey the fragment lifecycle and may continue running in the background | ||||||
|  |      * until it is complete. This is done due to RxJava2 no longer propagate errors once | ||||||
|  |      * an observer is unsubscribed while the thread process is still running. | ||||||
|  |      * | ||||||
|  |      * To solve the above issue, we can either set a global RxJava Error Handler, or | ||||||
|  |      * manage exceptions case by case. This should be done if the current implementation is | ||||||
|  |      * too costly when dealing with larger subscription sets. | ||||||
|  |      **/ | ||||||
|  |     private MaybeObserver<ChannelInfo> getChannelInfoObserver() { | ||||||
|  |         return new MaybeObserver<ChannelInfo>() { | ||||||
|  |             Disposable observer; | ||||||
|  |             @Override | ||||||
|  |             public void onSubscribe(Disposable d) { | ||||||
|  |                 observer = d; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Called only when response is non-empty | ||||||
|  |             @Override | ||||||
|  |             public void onSuccess(ChannelInfo channelInfo) { | ||||||
|  |                 emptyPanel.setVisibility(View.INVISIBLE); | ||||||
|  |  | ||||||
|  |                 if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) return; | ||||||
|  |  | ||||||
|  |                 final InfoItem item = channelInfo.related_streams.get(0); | ||||||
|  |                 // Keep requesting new items if the current one already exists | ||||||
|  |                 if (!doesItemExist(infoListAdapter.getItemsList(), item)) { | ||||||
|  |                     infoListAdapter.addInfoItem(item); | ||||||
|  |                 } else { | ||||||
|  |                     requestFeed(1); | ||||||
|  |                 } | ||||||
|  |                 onDone(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onError(Throwable exception) { | ||||||
|  |                 onRxError(exception, "Feed Display Reactor"); | ||||||
|  |                 onDone(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Called only when response is empty | ||||||
|  |             @Override | ||||||
|  |             public void onComplete() { | ||||||
|  |                 onDone(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             private void onDone() { | ||||||
|  |                 setLoader(true); | ||||||
|  |  | ||||||
|  |                 observer.dispose(); | ||||||
|  |                 observer = null; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) { | ||||||
|  |         for (final InfoItem existingItem: items) { | ||||||
|  |             if (existingItem.infoType() == item.infoType() && | ||||||
|  |                     existingItem.getTitle().equals(item.getTitle()) && | ||||||
|  |                     existingItem.getLink().equals(item.getLink())) return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void requestFeed(final int count) { | ||||||
|  |         if (feedSubscriber == null) return; | ||||||
|  |  | ||||||
|  |         feedSubscriber.request(count); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Disposable getLoadItemObserver(@NonNull final View itemLoader) { | ||||||
|  |         final Consumer<Object> onNext = new Consumer<Object>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(Object o) throws Exception { | ||||||
|  |                 requestFeed(FEED_LOAD_SIZE); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(Throwable throwable) throws Exception { | ||||||
|  |                 onRxError(throwable, "Load Button Reactor"); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return RxView.clicks(itemLoader) | ||||||
|  |                 .debounce(LOAD_ITEM_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) | ||||||
|  |                 .subscribe(onNext, onError); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment Error Handling | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     private void onRxError(final Throwable exception, final String tag) { | ||||||
|  |         if (exception instanceof IOException) { | ||||||
|  |             onRecoverableError(R.string.network_error); | ||||||
|  |         } else { | ||||||
|  |             onUnrecoverableError(exception, tag); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onRecoverableError(int messageId) { | ||||||
|  |         if (!this.isAdded()) return; | ||||||
|  |  | ||||||
|  |         if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); | ||||||
|  |         setErrorMessage(getString(messageId), true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onUnrecoverableError(Throwable exception, final String tag) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); | ||||||
|  |         ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "Feed", tag, R.string.general_error)); | ||||||
|  |  | ||||||
|  |         activity.finish(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -3,7 +3,11 @@ package org.schabi.newpipe.fragments; | |||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.support.annotation.Nullable; | import android.support.annotation.Nullable; | ||||||
|  | import android.support.design.widget.TabLayout; | ||||||
| import android.support.v4.app.Fragment; | import android.support.v4.app.Fragment; | ||||||
|  | import android.support.v4.app.FragmentManager; | ||||||
|  | import android.support.v4.app.FragmentPagerAdapter; | ||||||
|  | import android.support.v4.view.ViewPager; | ||||||
| import android.support.v7.app.ActionBar; | import android.support.v7.app.ActionBar; | ||||||
| import android.support.v7.app.AppCompatActivity; | import android.support.v7.app.AppCompatActivity; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| @@ -18,12 +22,14 @@ import org.schabi.newpipe.MainActivity; | |||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  |  | ||||||
| public class MainFragment extends Fragment { | public class MainFragment extends Fragment implements TabLayout.OnTabSelectedListener { | ||||||
|     private final String TAG = "MainFragment@" + Integer.toHexString(hashCode()); |     private final String TAG = "MainFragment@" + Integer.toHexString(hashCode()); | ||||||
|     private static final boolean DEBUG = MainActivity.DEBUG; |     private static final boolean DEBUG = MainActivity.DEBUG; | ||||||
|  |  | ||||||
|     private AppCompatActivity activity; |     private AppCompatActivity activity; | ||||||
|  |  | ||||||
|  |     private ViewPager viewPager; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Fragment's LifeCycle |     // Fragment's LifeCycle | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -45,7 +51,19 @@ public class MainFragment extends Fragment { | |||||||
|     @Override |     @Override | ||||||
|     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||||
|         if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); |         if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); | ||||||
|         return inflater.inflate(R.layout.fragment_main, container, false); |         View inflatedView = inflater.inflate(R.layout.fragment_main, container, false); | ||||||
|  |  | ||||||
|  |         TabLayout tabLayout = (TabLayout) inflatedView.findViewById(R.id.main_tab_layout); | ||||||
|  |         viewPager = (ViewPager) inflatedView.findViewById(R.id.pager); | ||||||
|  |  | ||||||
|  |         /*  Nested fragment, use child fragment here to maintain backstack in view pager. */ | ||||||
|  |         PagerAdapter adapter = new PagerAdapter(getChildFragmentManager()); | ||||||
|  |         viewPager.setAdapter(adapter); | ||||||
|  |         viewPager.setOffscreenPageLimit(adapter.getCount()); | ||||||
|  |  | ||||||
|  |         tabLayout.setupWithViewPager(viewPager); | ||||||
|  |  | ||||||
|  |         return inflatedView; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
| @@ -74,4 +92,47 @@ public class MainFragment extends Fragment { | |||||||
|         } |         } | ||||||
|         return super.onOptionsItemSelected(item); |         return super.onOptionsItemSelected(item); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onTabSelected(TabLayout.Tab tab) { | ||||||
|  |         viewPager.setCurrentItem(tab.getPosition()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onTabUnselected(TabLayout.Tab tab) {} | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onTabReselected(TabLayout.Tab tab) {} | ||||||
|  |  | ||||||
|  |     private class PagerAdapter extends FragmentPagerAdapter { | ||||||
|  |  | ||||||
|  |         private int[] tabTitles = new int[]{ | ||||||
|  |                 R.string.tab_main, | ||||||
|  |                 R.string.tab_subscriptions | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         PagerAdapter(FragmentManager fm) { | ||||||
|  |             super(fm); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public Fragment getItem(int position) { | ||||||
|  |             switch ( position ) { | ||||||
|  |                 case 1: | ||||||
|  |                     return new SubscriptionFragment(); | ||||||
|  |                 default: | ||||||
|  |                     return new BlankFragment(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public CharSequence getPageTitle(int position) { | ||||||
|  |             return getString(this.tabTitles[position]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public int getCount() { | ||||||
|  |             return this.tabTitles.length; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,278 @@ | |||||||
|  | package org.schabi.newpipe.fragments; | ||||||
|  |  | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Parcelable; | ||||||
|  | import android.support.annotation.Nullable; | ||||||
|  | import android.support.v7.widget.LinearLayoutManager; | ||||||
|  | import android.support.v7.widget.RecyclerView; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.MainActivity; | ||||||
|  | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  | import org.schabi.newpipe.extractor.InfoItem; | ||||||
|  | import org.schabi.newpipe.extractor.channel.ChannelInfoItem; | ||||||
|  | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
|  | import org.schabi.newpipe.info_list.InfoListAdapter; | ||||||
|  | import org.schabi.newpipe.report.ErrorActivity; | ||||||
|  | import org.schabi.newpipe.util.NavigationHelper; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Comparator; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import io.reactivex.Observer; | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import io.reactivex.disposables.Disposable; | ||||||
|  | import io.reactivex.schedulers.Schedulers; | ||||||
|  |  | ||||||
|  | import static org.schabi.newpipe.report.UserAction.REQUESTED_CHANNEL; | ||||||
|  | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
|  |  | ||||||
|  | public class SubscriptionFragment extends BaseFragment { | ||||||
|  |     private static final String VIEW_STATE_KEY = "view_state_key"; | ||||||
|  |     private final String TAG = "SubscriptionFragment@" + Integer.toHexString(hashCode()); | ||||||
|  |  | ||||||
|  |     private View inflatedView; | ||||||
|  |     private View emptyPanel; | ||||||
|  |     private View headerRootLayout; | ||||||
|  |     private View whatsNewView; | ||||||
|  |  | ||||||
|  |     private InfoListAdapter infoListAdapter; | ||||||
|  |     private RecyclerView resultRecyclerView; | ||||||
|  |     private Parcelable viewState; | ||||||
|  |  | ||||||
|  |     /* Used for independent events */ | ||||||
|  |     private CompositeDisposable disposables; | ||||||
|  |     private SubscriptionService subscriptionService; | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment LifeCycle | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |  | ||||||
|  |         disposables = new CompositeDisposable(); | ||||||
|  |         subscriptionService = SubscriptionService.getInstance( getContext() ); | ||||||
|  |  | ||||||
|  |         if (savedInstanceState != null) { | ||||||
|  |             viewState = savedInstanceState.getParcelable(VIEW_STATE_KEY); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         if (inflatedView == null) { | ||||||
|  |             inflatedView = inflater.inflate(R.layout.fragment_subscription, container, false); | ||||||
|  |         } | ||||||
|  |         return inflatedView; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onSaveInstanceState(Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |  | ||||||
|  |         outState.putParcelable(VIEW_STATE_KEY, viewState); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|  |         if (disposables != null) disposables.clear(); | ||||||
|  |  | ||||||
|  |         headerRootLayout = null; | ||||||
|  |         whatsNewView = null; | ||||||
|  |  | ||||||
|  |         super.onDestroyView(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         if (disposables != null) disposables.dispose(); | ||||||
|  |         disposables = null; | ||||||
|  |  | ||||||
|  |         subscriptionService = null; | ||||||
|  |  | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment Views | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     private RecyclerView.OnScrollListener getOnScrollListener() { | ||||||
|  |         return new RecyclerView.OnScrollListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onScrollStateChanged(RecyclerView recyclerView, int newState) { | ||||||
|  |                 super.onScrollStateChanged(recyclerView, newState); | ||||||
|  |                 if (newState == RecyclerView.SCROLL_STATE_IDLE) { | ||||||
|  |                     viewState = recyclerView.getLayoutManager().onSaveInstanceState(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private View.OnClickListener getWhatsNewOnClickListener() { | ||||||
|  |         return new View.OnClickListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onClick(View view) { | ||||||
|  |                 NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void initViews(View rootView, Bundle savedInstanceState) { | ||||||
|  |         super.initViews(rootView, savedInstanceState); | ||||||
|  |  | ||||||
|  |         emptyPanel = rootView.findViewById(R.id.empty_panel); | ||||||
|  |  | ||||||
|  |         resultRecyclerView = rootView.findViewById(R.id.result_list_view); | ||||||
|  |         resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); | ||||||
|  |         resultRecyclerView.addOnScrollListener(getOnScrollListener()); | ||||||
|  |  | ||||||
|  |         if (infoListAdapter == null) { | ||||||
|  |             infoListAdapter = new InfoListAdapter(getActivity()); | ||||||
|  |             infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false)); | ||||||
|  |             infoListAdapter.showFooter(false); | ||||||
|  |             infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { | ||||||
|  |                 @Override | ||||||
|  |                 public void selected(int serviceId, String url, String title) { | ||||||
|  |                     /* Requires the parent fragment to find holder for fragment replacement */ | ||||||
|  |                     NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), serviceId, url, title); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, resultRecyclerView, false); | ||||||
|  |         infoListAdapter.setHeader(headerRootLayout); | ||||||
|  |  | ||||||
|  |         whatsNewView = headerRootLayout.findViewById(R.id.whatsNew); | ||||||
|  |         whatsNewView.setOnClickListener(getWhatsNewOnClickListener()); | ||||||
|  |  | ||||||
|  |         resultRecyclerView.setAdapter(infoListAdapter); | ||||||
|  |  | ||||||
|  |         populateView(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void reloadContent() { | ||||||
|  |         populateView(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void setErrorMessage(String message, boolean showRetryButton) { | ||||||
|  |         super.setErrorMessage(message, showRetryButton); | ||||||
|  |         resetFragment(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void resetFragment() { | ||||||
|  |         if (disposables != null) disposables.clear(); | ||||||
|  |         if (infoListAdapter != null) infoListAdapter.clearStreamItemList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Subscriptions Loader | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     private void populateView() { | ||||||
|  |         resetFragment(); | ||||||
|  |  | ||||||
|  |         animateView(loadingProgressBar, true, 200); | ||||||
|  |         animateView(errorPanel, false, 200); | ||||||
|  |  | ||||||
|  |         subscriptionService.getSubscription().toObservable() | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(getSubscriptionObserver()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Observer<List<SubscriptionEntity>> getSubscriptionObserver() { | ||||||
|  |         return new Observer<List<SubscriptionEntity>>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSubscribe(Disposable d) { | ||||||
|  |                 animateView(loadingProgressBar, true, 200); | ||||||
|  |  | ||||||
|  |                 disposables.add( d ); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onNext(List<SubscriptionEntity> subscriptions) { | ||||||
|  |                 animateView(loadingProgressBar, true, 200); | ||||||
|  |  | ||||||
|  |                 infoListAdapter.clearStreamItemList(); | ||||||
|  |                 infoListAdapter.addInfoItemList( getSubscriptionItems(subscriptions) ); | ||||||
|  |  | ||||||
|  |                 animateView(loadingProgressBar, false, 200); | ||||||
|  |  | ||||||
|  |                 emptyPanel.setVisibility(subscriptions.isEmpty() ? View.VISIBLE : View.INVISIBLE); | ||||||
|  |  | ||||||
|  |                 if (viewState != null && resultRecyclerView != null) { | ||||||
|  |                     resultRecyclerView.getLayoutManager().onRestoreInstanceState(viewState); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onError(Throwable exception) { | ||||||
|  |                 if (exception instanceof IOException) { | ||||||
|  |                     onRecoverableError(R.string.network_error); | ||||||
|  |                 } else { | ||||||
|  |                     onUnrecoverableError(exception); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onComplete() { | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) { | ||||||
|  |         List<InfoItem> items = new ArrayList<>(); | ||||||
|  |         for (final SubscriptionEntity subscription: subscriptions) { | ||||||
|  |             ChannelInfoItem item = new ChannelInfoItem(); | ||||||
|  |             item.webPageUrl = subscription.getUrl(); | ||||||
|  |             item.serviceId = subscription.getServiceId(); | ||||||
|  |             item.channelName = subscription.getTitle(); | ||||||
|  |             item.thumbnailUrl = subscription.getThumbnailUrl(); | ||||||
|  |             item.subscriberCount = subscription.getSubscriberCount(); | ||||||
|  |             item.description = subscription.getDescription(); | ||||||
|  |  | ||||||
|  |             items.add( item ); | ||||||
|  |         } | ||||||
|  |         Collections.sort(items, new Comparator<InfoItem>() { | ||||||
|  |             @Override | ||||||
|  |             public int compare(InfoItem o1, InfoItem o2) { | ||||||
|  |                 return o1.getTitle().compareToIgnoreCase(o2.getTitle()); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return items; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Fragment Error Handling | ||||||
|  |     /////////////////////////////////////////////////////////////////////////// | ||||||
|  |  | ||||||
|  |     private void onRecoverableError(int messageId) { | ||||||
|  |         if (!this.isAdded()) return; | ||||||
|  |  | ||||||
|  |         if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]"); | ||||||
|  |         setErrorMessage(getString(messageId), true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void onUnrecoverableError(Throwable exception) { | ||||||
|  |         if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]"); | ||||||
|  |         ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(REQUESTED_CHANNEL, "unknown", "unknown", R.string.general_error)); | ||||||
|  |         activity.finish(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,170 @@ | |||||||
|  | package org.schabi.newpipe.fragments; | ||||||
|  |  | ||||||
|  | import android.content.Context; | ||||||
|  |  | ||||||
|  | import org.schabi.newpipe.NewPipeDatabase; | ||||||
|  | import org.schabi.newpipe.database.AppDatabase; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionDAO; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
|  | import org.schabi.newpipe.extractor.NewPipe; | ||||||
|  | import org.schabi.newpipe.extractor.StreamingService; | ||||||
|  | import org.schabi.newpipe.extractor.channel.ChannelExtractor; | ||||||
|  | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
|  | import org.schabi.newpipe.extractor.exceptions.ExtractionException; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.Callable; | ||||||
|  | import java.util.concurrent.Executor; | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.CompletableSource; | ||||||
|  | import io.reactivex.Flowable; | ||||||
|  | import io.reactivex.Maybe; | ||||||
|  | import io.reactivex.Scheduler; | ||||||
|  | import io.reactivex.annotations.NonNull; | ||||||
|  | import io.reactivex.functions.Function; | ||||||
|  | import io.reactivex.schedulers.Schedulers; | ||||||
|  |  | ||||||
|  | /** Subscription Service singleton: | ||||||
|  |  *  Provides a basis for channel Subscriptions. | ||||||
|  |  *  Provides access to subscription table in database as well as | ||||||
|  |  *  up-to-date observations on the subscribed channels | ||||||
|  |  *  */ | ||||||
|  | public class SubscriptionService { | ||||||
|  |  | ||||||
|  |     private static SubscriptionService sInstance; | ||||||
|  |     private static final Object LOCK = new Object(); | ||||||
|  |  | ||||||
|  |     public static SubscriptionService getInstance(Context context) { | ||||||
|  |         if (sInstance == null) { | ||||||
|  |             synchronized (LOCK) { | ||||||
|  |                 if (sInstance == null) { | ||||||
|  |                     sInstance = new SubscriptionService(context); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return sInstance; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode()); | ||||||
|  |     private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500; | ||||||
|  |     private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4; | ||||||
|  |  | ||||||
|  |     private AppDatabase db; | ||||||
|  |     private Flowable<List<SubscriptionEntity>> subscription; | ||||||
|  |  | ||||||
|  |     private Scheduler subscriptionScheduler; | ||||||
|  |  | ||||||
|  |     private SubscriptionService(Context context) { | ||||||
|  |         db = NewPipeDatabase.getInstance( context ); | ||||||
|  |         subscription = getSubscriptionInfos(); | ||||||
|  |  | ||||||
|  |         final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE); | ||||||
|  |         subscriptionScheduler = Schedulers.from(subscriptionExecutor); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Part of subscription observation pipeline | ||||||
|  |      * @see SubscriptionService#getSubscription() | ||||||
|  |      */ | ||||||
|  |     private Flowable<List<SubscriptionEntity>> getSubscriptionInfos() { | ||||||
|  |         return subscriptionTable().findAll() | ||||||
|  |                 // Wait for a period of infrequent updates and return the latest update | ||||||
|  |                 .debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) | ||||||
|  |                 .share()            // Share allows multiple subscribers on the same observable | ||||||
|  |                 .replay(1)          // Replay synchronizes subscribers to the last emitted result | ||||||
|  |                 .autoConnect(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Provides an observer to the latest update to the subscription table. | ||||||
|  |      * | ||||||
|  |      *  This observer may be subscribed multiple times, where each subscriber obtains | ||||||
|  |      *  the latest synchronized changes available, effectively share the same data | ||||||
|  |      *  across all subscribers. | ||||||
|  |      * | ||||||
|  |      *  This observer has a debounce cooldown, meaning if multiple updates are observed | ||||||
|  |      *  in the cooldown interval, only the latest changes are emitted to the subscribers. | ||||||
|  |      *  This reduces the amount of observations caused by frequent updates to the database. | ||||||
|  |      *  */ | ||||||
|  |     @android.support.annotation.NonNull | ||||||
|  |     public Flowable<List<SubscriptionEntity>> getSubscription() { | ||||||
|  |         return subscription; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) { | ||||||
|  |         final StreamingService service = getService(subscriptionEntity.getServiceId()); | ||||||
|  |         if (service == null) return Maybe.empty(); | ||||||
|  |  | ||||||
|  |         final String url = subscriptionEntity.getUrl(); | ||||||
|  |         final Callable<ChannelInfo> callable = new Callable<ChannelInfo>() { | ||||||
|  |             @Override | ||||||
|  |             public ChannelInfo call() throws Exception { | ||||||
|  |                 final ChannelExtractor extractor = service.getChannelExtractorInstance(url, 0); | ||||||
|  |                 return ChannelInfo.getInfo(extractor); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return Maybe.fromCallable(callable).subscribeOn(subscriptionScheduler); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private StreamingService getService(final int serviceId) { | ||||||
|  |         try { | ||||||
|  |             return NewPipe.getService(serviceId); | ||||||
|  |         } catch (ExtractionException e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Returns the database access interface for subscription table. */ | ||||||
|  |     public SubscriptionDAO subscriptionTable() { | ||||||
|  |         return db.subscriptionDAO(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Completable updateChannelInfo(final int serviceId, | ||||||
|  |                                           final String channelUrl, | ||||||
|  |                                           final ChannelInfo info) { | ||||||
|  |         final Function<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() { | ||||||
|  |             @Override | ||||||
|  |             public CompletableSource apply(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception { | ||||||
|  |                 if (subscriptionEntities.size() == 1) { | ||||||
|  |                     SubscriptionEntity subscription = subscriptionEntities.get(0); | ||||||
|  |  | ||||||
|  |                     // Subscriber count changes very often, making this check almost unnecessary. | ||||||
|  |                     // Consider removing it later. | ||||||
|  |                     if (isSubscriptionUpToDate(channelUrl, info, subscription)) { | ||||||
|  |                         subscription.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); | ||||||
|  |  | ||||||
|  |                         return update(subscription); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return Completable.complete(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return subscriptionTable().findAll(serviceId, channelUrl) | ||||||
|  |                 .firstOrError() | ||||||
|  |                 .flatMapCompletable(update); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Completable update(final SubscriptionEntity updatedSubscription) { | ||||||
|  |         return Completable.fromRunnable(new Runnable() { | ||||||
|  |             @Override | ||||||
|  |             public void run() { | ||||||
|  |                 subscriptionTable().update(updatedSubscription); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private boolean isSubscriptionUpToDate(final String channelUrl, | ||||||
|  |                                            final ChannelInfo info, | ||||||
|  |                                            final SubscriptionEntity entity) { | ||||||
|  |         return channelUrl.equals( entity.getUrl() ) && | ||||||
|  |                 info.service_id == entity.getServiceId() && | ||||||
|  |                 info.channel_name.equals( entity.getTitle() ) && | ||||||
|  |                 info.avatar_url.equals( entity.getThumbnailUrl() ) && | ||||||
|  |                 info.subscriberCount == entity.getSubscriberCount(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -20,12 +20,17 @@ import android.view.ViewGroup; | |||||||
| import android.widget.Button; | import android.widget.Button; | ||||||
| import android.widget.ImageView; | import android.widget.ImageView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
|  |  | ||||||
|  | import com.jakewharton.rxbinding2.view.RxView; | ||||||
|  |  | ||||||
| import org.schabi.newpipe.ImageErrorLoadingListener; | import org.schabi.newpipe.ImageErrorLoadingListener; | ||||||
| import org.schabi.newpipe.R; | import org.schabi.newpipe.R; | ||||||
|  | import org.schabi.newpipe.database.subscription.SubscriptionEntity; | ||||||
| import org.schabi.newpipe.extractor.InfoItem; | import org.schabi.newpipe.extractor.InfoItem; | ||||||
| import org.schabi.newpipe.extractor.channel.ChannelInfo; | import org.schabi.newpipe.extractor.channel.ChannelInfo; | ||||||
| import org.schabi.newpipe.fragments.BaseFragment; | import org.schabi.newpipe.fragments.BaseFragment; | ||||||
|  | import org.schabi.newpipe.fragments.SubscriptionService; | ||||||
| import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; | import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener; | ||||||
| import org.schabi.newpipe.info_list.InfoItemBuilder; | import org.schabi.newpipe.info_list.InfoItemBuilder; | ||||||
| import org.schabi.newpipe.info_list.InfoListAdapter; | import org.schabi.newpipe.info_list.InfoListAdapter; | ||||||
| @@ -36,16 +41,30 @@ import org.schabi.newpipe.workers.ChannelExtractorWorker; | |||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| import java.text.NumberFormat; | import java.text.NumberFormat; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import io.reactivex.Observer; | ||||||
|  | import io.reactivex.android.schedulers.AndroidSchedulers; | ||||||
|  | import io.reactivex.annotations.NonNull; | ||||||
|  | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import io.reactivex.disposables.Disposable; | ||||||
|  | import io.reactivex.functions.Action; | ||||||
|  | import io.reactivex.functions.Consumer; | ||||||
|  | import io.reactivex.functions.Function; | ||||||
|  | import io.reactivex.schedulers.Schedulers; | ||||||
|  |  | ||||||
| import static org.schabi.newpipe.util.AnimationUtils.animateView; | import static org.schabi.newpipe.util.AnimationUtils.animateView; | ||||||
|  |  | ||||||
| public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive { | public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive { | ||||||
|     private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode()); | private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode()); | ||||||
|  |  | ||||||
|     private static final String INFO_LIST_KEY = "info_list_key"; |     private static final String INFO_LIST_KEY = "info_list_key"; | ||||||
|     private static final String CHANNEL_INFO_KEY = "channel_info_key"; |     private static final String CHANNEL_INFO_KEY = "channel_info_key"; | ||||||
|     private static final String PAGE_NUMBER_KEY = "page_number_key"; |     private static final String PAGE_NUMBER_KEY = "page_number_key"; | ||||||
|  |  | ||||||
|  |     private static final int BUTTON_DEBOUNCE_INTERVAL = 100; | ||||||
|  |  | ||||||
|     private InfoListAdapter infoListAdapter; |     private InfoListAdapter infoListAdapter; | ||||||
|  |  | ||||||
|     private ChannelExtractorWorker currentChannelWorker; |     private ChannelExtractorWorker currentChannelWorker; | ||||||
| @@ -53,9 +72,15 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|     private int serviceId = -1; |     private int serviceId = -1; | ||||||
|     private String channelName = ""; |     private String channelName = ""; | ||||||
|     private String channelUrl = ""; |     private String channelUrl = ""; | ||||||
|  |     private String feedUrl = ""; | ||||||
|     private int pageNumber = 0; |     private int pageNumber = 0; | ||||||
|     private boolean hasNextPage = true; |     private boolean hasNextPage = true; | ||||||
|  |  | ||||||
|  |     private SubscriptionService subscriptionService; | ||||||
|  |  | ||||||
|  |     private CompositeDisposable disposables; | ||||||
|  |     private Disposable subscribeButtonMonitor; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Views |     // Views | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -67,7 +92,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|     private ImageView headerAvatarView; |     private ImageView headerAvatarView; | ||||||
|     private TextView headerTitleView; |     private TextView headerTitleView; | ||||||
|     private TextView headerSubscribersTextView; |     private TextView headerSubscribersTextView; | ||||||
|     private Button headerRssButton; |     private Button headerSubscribeButton; | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////*/ |     /*////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
| @@ -127,7 +152,13 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|         headerAvatarView = null; |         headerAvatarView = null; | ||||||
|         headerTitleView = null; |         headerTitleView = null; | ||||||
|         headerSubscribersTextView = null; |         headerSubscribersTextView = null; | ||||||
|         headerRssButton = null; |         headerSubscribeButton = null; | ||||||
|  |  | ||||||
|  |         if (disposables != null) disposables.dispose(); | ||||||
|  |         if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); | ||||||
|  |         disposables = null; | ||||||
|  |         subscribeButtonMonitor = null; | ||||||
|  |         subscriptionService = null; | ||||||
|  |  | ||||||
|         super.onDestroyView(); |         super.onDestroyView(); | ||||||
|     } |     } | ||||||
| @@ -176,6 +207,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|             supportActionBar.setDisplayShowTitleEnabled(true); |             supportActionBar.setDisplayShowTitleEnabled(true); | ||||||
|             supportActionBar.setDisplayHomeAsUpEnabled(true); |             supportActionBar.setDisplayHomeAsUpEnabled(true); | ||||||
|         } |         } | ||||||
|  |         menu.findItem(R.id.menu_item_rss).setVisible( !TextUtils.isEmpty(feedUrl) ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -190,13 +222,21 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|                 startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); |                 startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|             case R.id.menu_item_share: |             case R.id.menu_item_rss: { | ||||||
|  |                 Intent intent = new Intent(); | ||||||
|  |                 intent.setAction(Intent.ACTION_VIEW); | ||||||
|  |                 intent.setData(Uri.parse(currentChannelInfo.feed_url)); | ||||||
|  |                 startActivity(intent); | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |             case R.id.menu_item_share: { | ||||||
|                 Intent intent = new Intent(); |                 Intent intent = new Intent(); | ||||||
|                 intent.setAction(Intent.ACTION_SEND); |                 intent.setAction(Intent.ACTION_SEND); | ||||||
|                 intent.putExtra(Intent.EXTRA_TEXT, channelUrl); |                 intent.putExtra(Intent.EXTRA_TEXT, channelUrl); | ||||||
|                 intent.setType("text/plain"); |                 intent.setType("text/plain"); | ||||||
|                 startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); |                 startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); | ||||||
|                 return true; |                 return true; | ||||||
|  |             } | ||||||
|             default: |             default: | ||||||
|                 return super.onOptionsItemSelected(item); |                 return super.onOptionsItemSelected(item); | ||||||
|         } |         } | ||||||
| @@ -231,7 +271,10 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|         headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view); |         headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view); | ||||||
|         headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view); |         headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view); | ||||||
|         headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view); |         headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view); | ||||||
|         headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button); |         headerSubscribeButton = (Button) headerRootLayout.findViewById(R.id.channel_subscribe_button); | ||||||
|  |  | ||||||
|  |         disposables = new CompositeDisposable(); | ||||||
|  |         subscriptionService = SubscriptionService.getInstance( getContext() ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void initListeners() { |     protected void initListeners() { | ||||||
| @@ -255,16 +298,8 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|         headerRssButton.setOnClickListener(new View.OnClickListener() { |  | ||||||
|             @Override |  | ||||||
|             public void onClick(View view) { |  | ||||||
|                 if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url); |  | ||||||
|                 Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url)); |  | ||||||
|                 startActivity(i); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void reloadContent() { |     protected void reloadContent() { | ||||||
| @@ -274,6 +309,133 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|         loadPage(0); |         loadPage(0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|  |     // Channel Subscription | ||||||
|  |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|  |  | ||||||
|  |     private void monitorSubscription(final int serviceId, | ||||||
|  |                                      final String channelUrl, | ||||||
|  |                                      final ChannelInfo info) { | ||||||
|  |         subscriptionService.subscriptionTable().findAll(serviceId, channelUrl) | ||||||
|  |                 .toObservable() | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(getSubscribeButtonMonitor(serviceId, channelUrl, info)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) { | ||||||
|  |         return new Function<Object, Object>() { | ||||||
|  |             @Override | ||||||
|  |             public Object apply(@NonNull Object o) throws Exception { | ||||||
|  |                 subscriptionService.subscriptionTable().insert( subscription ); | ||||||
|  |                 return o; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) { | ||||||
|  |         return new Function<Object, Object>() { | ||||||
|  |             @Override | ||||||
|  |             public Object apply(@NonNull Object o) throws Exception { | ||||||
|  |                 subscriptionService.subscriptionTable().delete( subscription ); | ||||||
|  |                 return o; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Observer<List<SubscriptionEntity>> getSubscribeButtonMonitor(final int serviceId, | ||||||
|  |                                                                          final String channelUrl, | ||||||
|  |                                                                          final ChannelInfo info) { | ||||||
|  |         return new Observer<List<SubscriptionEntity>>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSubscribe(Disposable d) { | ||||||
|  |                 disposables.add( d ); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onNext(List<SubscriptionEntity> subscriptionEntities) { | ||||||
|  |                 if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); | ||||||
|  |  | ||||||
|  |                 if (subscriptionEntities.isEmpty()) { | ||||||
|  |                     if (DEBUG) Log.d(TAG, "No subscription to this channel!"); | ||||||
|  |                     SubscriptionEntity channel = new SubscriptionEntity(); | ||||||
|  |                     channel.setServiceId( serviceId ); | ||||||
|  |                     channel.setUrl( channelUrl ); | ||||||
|  |                     channel.setData(info.channel_name, info.avatar_url, "", info.subscriberCount); | ||||||
|  |  | ||||||
|  |                     subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); | ||||||
|  |  | ||||||
|  |                     headerSubscribeButton.setText(R.string.subscribe_button_title); | ||||||
|  |                 } else { | ||||||
|  |                     if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); | ||||||
|  |                     final SubscriptionEntity subscription = subscriptionEntities.get(0); | ||||||
|  |                     subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); | ||||||
|  |  | ||||||
|  |                     headerSubscribeButton.setText(R.string.subscribed_button_title); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 headerSubscribeButton.setVisibility(View.VISIBLE); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onError(Throwable throwable) { | ||||||
|  |                 Log.e(TAG, "Status get failed", throwable); | ||||||
|  |                 headerSubscribeButton.setVisibility(View.INVISIBLE); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             @Override | ||||||
|  |             public void onComplete() {} | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Disposable monitorSubscribeButton(final Button subscribeButton, | ||||||
|  |                                               final Function<Object, Object> action) { | ||||||
|  |         final Consumer<Object> onNext = new Consumer<Object>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(@NonNull Object o) throws Exception { | ||||||
|  |                 if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(@NonNull Throwable throwable) throws Exception { | ||||||
|  |                 if (DEBUG) Log.e(TAG, "Subscription Fatal Error: ", throwable.getCause()); | ||||||
|  |                 Toast.makeText(getContext(), R.string.subscription_change_failed, Toast.LENGTH_SHORT).show(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         /* Emit clicks from main thread unto io thread */ | ||||||
|  |         return RxView.clicks(subscribeButton) | ||||||
|  |                 .subscribeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .observeOn(Schedulers.io()) | ||||||
|  |                 .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks | ||||||
|  |                 .map(action) | ||||||
|  |                 .subscribe(onNext, onError); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Disposable updateSubscription(final int serviceId, | ||||||
|  |                                           final String channelUrl, | ||||||
|  |                                           final ChannelInfo info) { | ||||||
|  |         final Action onComplete = new Action() { | ||||||
|  |             @Override | ||||||
|  |             public void run() throws Exception { | ||||||
|  |                 if (DEBUG) Log.d(TAG, "Updated subscription: " + channelUrl); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         final Consumer<Throwable> onError = new Consumer<Throwable>() { | ||||||
|  |             @Override | ||||||
|  |             public void accept(@NonNull Throwable throwable) throws Exception { | ||||||
|  |                 Log.e(TAG, "Subscription Update Fatal Error: ", throwable); | ||||||
|  |                 Toast.makeText(getContext(), R.string.subscription_update_failed, Toast.LENGTH_SHORT).show(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         return subscriptionService.updateChannelInfo(serviceId, channelUrl, info) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(onComplete, onError); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Utils |     // Utils | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
| @@ -297,7 +459,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|         imageLoader.cancelDisplayTask(headerChannelBanner); |         imageLoader.cancelDisplayTask(headerChannelBanner); | ||||||
|         imageLoader.cancelDisplayTask(headerAvatarView); |         imageLoader.cancelDisplayTask(headerAvatarView); | ||||||
|  |  | ||||||
|         headerRssButton.setVisibility(View.GONE); |         headerSubscribeButton.setVisibility(View.GONE); | ||||||
|         headerSubscribersTextView.setVisibility(View.GONE); |         headerSubscribersTextView.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|         headerTitleView.setText(channelName != null ? channelName : ""); |         headerTitleView.setText(channelName != null ? channelName : ""); | ||||||
| @@ -331,6 +493,9 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|         animateView(loadingProgressBar, false, 200); |         animateView(loadingProgressBar, false, 200); | ||||||
|  |  | ||||||
|         if (!onlyVideos) { |         if (!onlyVideos) { | ||||||
|  |             feedUrl = info.feed_url; | ||||||
|  |             if (activity.getSupportActionBar() != null) activity.getSupportActionBar().invalidateOptionsMenu(); | ||||||
|  |  | ||||||
|             headerRootLayout.setVisibility(View.VISIBLE); |             headerRootLayout.setVisibility(View.VISIBLE); | ||||||
|             //animateView(loadingProgressBar, false, 200, null); |             //animateView(loadingProgressBar, false, 200, null); | ||||||
|  |  | ||||||
| @@ -354,8 +519,10 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor | |||||||
|                 headerSubscribersTextView.setVisibility(View.VISIBLE); |                 headerSubscribersTextView.setVisibility(View.VISIBLE); | ||||||
|             } else headerSubscribersTextView.setVisibility(View.GONE); |             } else headerSubscribersTextView.setVisibility(View.GONE); | ||||||
|  |  | ||||||
|             if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE); |             if (disposables != null) disposables.clear(); | ||||||
|             else headerRssButton.setVisibility(View.INVISIBLE); |             if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); | ||||||
|  |             disposables.add( updateSubscription(serviceId, channelUrl, info) ); | ||||||
|  |             monitorSubscription(serviceId, channelUrl, info); | ||||||
|  |  | ||||||
|             infoListAdapter.showFooter(true); |             infoListAdapter.showFooter(true); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -231,14 +231,13 @@ public class InfoItemBuilder { | |||||||
|         holder.itemRoot.setOnClickListener(new View.OnClickListener() { |         holder.itemRoot.setOnClickListener(new View.OnClickListener() { | ||||||
|             @Override |             @Override | ||||||
|             public void onClick(View view) { |             public void onClick(View view) { | ||||||
|                 if(onStreamInfoItemSelectedListener != null) { |                 if(onChannelInfoItemSelectedListener != null) { | ||||||
|                     onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); |                     onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     public String shortViewCount(Long viewCount) { |     public String shortViewCount(Long viewCount) { | ||||||
|         if (viewCount >= 1000000000) { |         if (viewCount >= 1000000000) { | ||||||
|             return Long.toString(viewCount / 1000000000) + billion + " " + viewsS; |             return Long.toString(viewCount / 1000000000) + billion + " " + viewsS; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list; | |||||||
|  |  | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.support.v7.widget.RecyclerView; | import android.support.v7.widget.RecyclerView; | ||||||
|  | import android.text.Layout; | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| @@ -9,6 +10,7 @@ import android.view.ViewGroup; | |||||||
|  |  | ||||||
| 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 java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @@ -77,6 +79,13 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void addInfoItem(InfoItem data) { | ||||||
|  |         if (data != null) { | ||||||
|  |             infoItemList.add( data ); | ||||||
|  |             notifyDataSetChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public void clearStreamItemList() { |     public void clearStreamItemList() { | ||||||
|         if(infoItemList.isEmpty()) { |         if(infoItemList.isEmpty()) { | ||||||
|             return; |             return; | ||||||
| @@ -118,7 +127,8 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde | |||||||
|         if(footer != null && position == infoItemList.size() && showFooter) { |         if(footer != null && position == infoItemList.size() && showFooter) { | ||||||
|             return 1; |             return 1; | ||||||
|         } |         } | ||||||
|         switch(infoItemList.get(position).infoType()) { |         InfoItem item = infoItemList.get(position); | ||||||
|  |         switch(item.infoType()) { | ||||||
|             case STREAM: |             case STREAM: | ||||||
|                 return 2; |                 return 2; | ||||||
|             case CHANNEL: |             case CHANNEL: | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ import org.schabi.newpipe.extractor.NewPipe; | |||||||
| import org.schabi.newpipe.extractor.StreamingService; | import org.schabi.newpipe.extractor.StreamingService; | ||||||
| import org.schabi.newpipe.extractor.stream_info.AudioStream; | import org.schabi.newpipe.extractor.stream_info.AudioStream; | ||||||
| import org.schabi.newpipe.extractor.stream_info.StreamInfo; | import org.schabi.newpipe.extractor.stream_info.StreamInfo; | ||||||
|  | import org.schabi.newpipe.fragments.FeedFragment; | ||||||
| import org.schabi.newpipe.fragments.MainFragment; | import org.schabi.newpipe.fragments.MainFragment; | ||||||
| import org.schabi.newpipe.fragments.channel.ChannelFragment; | import org.schabi.newpipe.fragments.channel.ChannelFragment; | ||||||
| import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | import org.schabi.newpipe.fragments.detail.VideoDetailFragment; | ||||||
| @@ -139,6 +140,14 @@ public class NavigationHelper { | |||||||
|                 .commit(); |                 .commit(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static void openWhatsNewFragment(FragmentManager fragmentManager) { | ||||||
|  |         fragmentManager.beginTransaction() | ||||||
|  |                 .setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out, R.anim.custom_fade_in, R.anim.custom_fade_out) | ||||||
|  |                 .replace(R.id.fragment_holder, new FeedFragment()) | ||||||
|  |                 .addToBackStack(null) | ||||||
|  |                 .commit(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /*////////////////////////////////////////////////////////////////////////// |     /*////////////////////////////////////////////////////////////////////////// | ||||||
|     // Through Intents |     // Through Intents | ||||||
|     //////////////////////////////////////////////////////////////////////////*/ |     //////////////////////////////////////////////////////////////////////////*/ | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ | |||||||
|         tools:visibility="visible"/> |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
|     <Button |     <Button | ||||||
|         android:id="@+id/channel_rss_button" |         android:id="@+id/channel_subscribe_button" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_alignParentRight="true" |         android:layout_alignParentRight="true" | ||||||
| @@ -76,8 +76,7 @@ | |||||||
|         android:layout_gravity="center_vertical|right" |         android:layout_gravity="center_vertical|right" | ||||||
|         android:layout_marginRight="4dp" |         android:layout_marginRight="4dp" | ||||||
|         android:layout_marginTop="2dp" |         android:layout_marginTop="2dp" | ||||||
|         android:drawableLeft="@drawable/ic_rss_feed_white_24dp" |         android:text="@string/subscribe_button_title" | ||||||
|         android:text="@string/rss_button_title" |  | ||||||
|         android:textSize="@dimen/channel_rss_title_size" |         android:textSize="@dimen/channel_rss_title_size" | ||||||
|         android:theme="@style/RedButton" |         android:theme="@style/RedButton" | ||||||
|         android:visibility="gone" |         android:visibility="gone" | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ | |||||||
|         android:layout_below="@id/channel_banner_image" |         android:layout_below="@id/channel_banner_image" | ||||||
|         android:layout_marginLeft="8dp" |         android:layout_marginLeft="8dp" | ||||||
|         android:layout_marginTop="6dp" |         android:layout_marginTop="6dp" | ||||||
|         android:layout_toLeftOf="@+id/channel_rss_button" |         android:layout_toLeftOf="@+id/channel_subscribe_button" | ||||||
|         android:layout_toRightOf="@+id/channel_avatar_layout" |         android:layout_toRightOf="@+id/channel_avatar_layout" | ||||||
|         android:textAppearance="?android:attr/textAppearanceLarge" |         android:textAppearance="?android:attr/textAppearanceLarge" | ||||||
|         android:textSize="@dimen/video_item_detail_title_text_size" |         android:textSize="@dimen/video_item_detail_title_text_size" | ||||||
| @@ -70,15 +70,14 @@ | |||||||
|         tools:visibility="visible"/> |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
|     <Button |     <Button | ||||||
|         android:id="@+id/channel_rss_button" |         android:id="@+id/channel_subscribe_button" | ||||||
|         android:layout_width="wrap_content" |         android:layout_width="wrap_content" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_alignParentRight="true" |         android:layout_alignParentRight="true" | ||||||
|         android:layout_below="@+id/channel_banner_image" |         android:layout_below="@+id/channel_banner_image" | ||||||
|         android:layout_gravity="center_vertical|right" |         android:layout_gravity="center_vertical|right" | ||||||
|         android:layout_marginRight="2dp" |         android:layout_marginRight="2dp" | ||||||
|         android:drawableLeft="@drawable/ic_rss_feed_white_24dp" |         android:text="@string/subscribe_button_title" | ||||||
|         android:text="@string/rss_button_title" |  | ||||||
|         android:textSize="@dimen/channel_rss_title_size" |         android:textSize="@dimen/channel_rss_title_size" | ||||||
|         android:theme="@style/RedButton" |         android:theme="@style/RedButton" | ||||||
|         android:visibility="gone" |         android:visibility="gone" | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								app/src/main/res/layout/empty_view_panel.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/res/layout/empty_view_panel.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:orientation="vertical" | ||||||
|  |     android:gravity="center"> | ||||||
|  |     <TextView | ||||||
|  |         android:textAppearance="?android:attr/textAppearanceLarge" | ||||||
|  |         android:text="¯\\_(ツ)_/¯" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" /> | ||||||
|  |     <TextView | ||||||
|  |         android:text="Nothing Here But Crickets" | ||||||
|  |         android:layout_gravity="center" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" /> | ||||||
|  | </LinearLayout> | ||||||
							
								
								
									
										19
									
								
								app/src/main/res/layout/fragment_blank.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/res/layout/fragment_blank.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|  |  | ||||||
|  |     <include layout="@layout/main_bg" /> | ||||||
|  |  | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/error_panel" | ||||||
|  |         layout="@layout/error_retry" | ||||||
|  |         tools:visibility="visible" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerHorizontal="true" | ||||||
|  |         android:layout_marginTop="50dp" | ||||||
|  |         android:visibility="gone" /> | ||||||
|  |  | ||||||
|  | </RelativeLayout> | ||||||
| @@ -32,4 +32,14 @@ | |||||||
|         android:visibility="gone" |         android:visibility="gone" | ||||||
|         tools:visibility="visible" /> |         tools:visibility="visible" /> | ||||||
|  |  | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/empty_panel" | ||||||
|  |         layout="@layout/empty_view_panel" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:layout_marginTop="50dp" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
| </RelativeLayout> | </RelativeLayout> | ||||||
|   | |||||||
| @@ -1,8 +1,21 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     android:layout_height="match_parent" |     android:layout_height="match_parent" | ||||||
|     android:layout_width="match_parent"> |     android:layout_width="match_parent" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||||
|  |  | ||||||
|     <include layout="@layout/main_bg" /> |  | ||||||
|  |     <android.support.design.widget.TabLayout | ||||||
|  |         android:id="@+id/main_tab_layout" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_alignParentBottom="true" | ||||||
|  |         app:tabGravity="fill"/> | ||||||
|  |  | ||||||
|  |     <android.support.v4.view.ViewPager | ||||||
|  |         android:id="@+id/pager" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="fill_parent" | ||||||
|  |         android:layout_above="@id/main_tab_layout"/> | ||||||
|  |  | ||||||
| </RelativeLayout> | </RelativeLayout> | ||||||
|   | |||||||
							
								
								
									
										49
									
								
								app/src/main/res/layout/fragment_subscription.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/src/main/res/layout/fragment_subscription.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:focusable="true" | ||||||
|  |     android:focusableInTouchMode="true"> | ||||||
|  |  | ||||||
|  |     <android.support.v7.widget.RecyclerView | ||||||
|  |         android:id="@+id/result_list_view" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         android:scrollbars="vertical" | ||||||
|  |         app:layoutManager="LinearLayoutManager" | ||||||
|  |         tools:listitem="@layout/channel_item"/> | ||||||
|  |  | ||||||
|  |     <ProgressBar | ||||||
|  |         android:id="@+id/loading_progress_bar" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:indeterminate="true" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
|  |     <!--ERROR PANEL--> | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/error_panel" | ||||||
|  |         layout="@layout/error_retry" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerHorizontal="true" | ||||||
|  |         android:layout_marginTop="50dp" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
|  |     <include | ||||||
|  |         android:id="@+id/empty_panel" | ||||||
|  |         layout="@layout/empty_view_panel" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:layout_marginTop="50dp" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         tools:visibility="visible"/> | ||||||
|  |  | ||||||
|  | </RelativeLayout> | ||||||
							
								
								
									
										28
									
								
								app/src/main/res/layout/load_item_footer.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/src/main/res/layout/load_item_footer.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:id="@+id/itemRoot" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="@dimen/video_item_search_height" | ||||||
|  |     android:background="?attr/selectableItemBackground" | ||||||
|  |     android:clickable="false" | ||||||
|  |     android:padding="@dimen/video_item_search_padding"> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/load_more_text" | ||||||
|  |         android:text="Load More" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:textAppearance="?android:attr/textAppearanceLarge" | ||||||
|  |         android:textSize="@dimen/header_footer_text_size" | ||||||
|  |         android:visibility="gone"/> | ||||||
|  |  | ||||||
|  |     <ProgressBar | ||||||
|  |         android:id="@+id/paginate_progress_bar" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_centerInParent="true" | ||||||
|  |         android:visibility="gone"/> | ||||||
|  |  | ||||||
|  | </RelativeLayout> | ||||||
							
								
								
									
										33
									
								
								app/src/main/res/layout/subscription_header.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/src/main/res/layout/subscription_header.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <RelativeLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:id="@+id/channel_header_layout" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="wrap_content" | ||||||
|  |     android:layout_marginBottom="12dp"> | ||||||
|  |  | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/whatsNew" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="50dp" | ||||||
|  |         android:paddingLeft="12dp" | ||||||
|  |         android:paddingRight="12dp" | ||||||
|  |         android:drawableLeft="?attr/rss" | ||||||
|  |         android:drawablePadding="5dp" | ||||||
|  |         android:text="@string/fragment_whats_new" | ||||||
|  |         android:textAppearance="?android:attr/textAppearanceLarge" | ||||||
|  |         android:textSize="@dimen/header_footer_text_size" | ||||||
|  |         android:gravity="left|center" | ||||||
|  |         android:clickable="true" | ||||||
|  |         tools:ignore="RtlHardcoded"/> | ||||||
|  |  | ||||||
|  |     <View | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="1px" | ||||||
|  |         android:layout_marginLeft="8dp" | ||||||
|  |         android:layout_marginRight="8dp" | ||||||
|  |         android:layout_below="@id/whatsNew" | ||||||
|  |         android:background="?attr/colorAccent" /> | ||||||
|  |  | ||||||
|  | </RelativeLayout> | ||||||
| @@ -7,6 +7,11 @@ | |||||||
|         app:showAsAction="never" |         app:showAsAction="never" | ||||||
|         android:title="@string/open_in_browser" /> |         android:title="@string/open_in_browser" /> | ||||||
|  |  | ||||||
|  |     <item android:id="@+id/menu_item_rss" | ||||||
|  |         app:showAsAction="ifRoom" | ||||||
|  |         android:title="@string/rss_button_title" | ||||||
|  |         android:icon="?attr/rss"/> | ||||||
|  |  | ||||||
|     <item android:id="@+id/menu_item_share" |     <item android:id="@+id/menu_item_share" | ||||||
|         android:title="@string/share" |         android:title="@string/share" | ||||||
|         app:showAsAction="ifRoom" |         app:showAsAction="ifRoom" | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|     <dimen name="video_item_search_duration_text_size">12sp</dimen> |     <dimen name="video_item_search_duration_text_size">12sp</dimen> | ||||||
|     <dimen name="video_item_search_uploader_text_size">14sp</dimen> |     <dimen name="video_item_search_uploader_text_size">14sp</dimen> | ||||||
|     <dimen name="video_item_search_upload_date_text_size">14sp</dimen> |     <dimen name="video_item_search_upload_date_text_size">14sp</dimen> | ||||||
|  |     <dimen name="header_footer_text_size">18sp</dimen> | ||||||
|     <!-- Elements Size --> |     <!-- Elements Size --> | ||||||
|     <!-- 16 / 9 ratio--> |     <!-- 16 / 9 ratio--> | ||||||
|     <dimen name="video_item_search_thumbnail_image_width">142dp</dimen> |     <dimen name="video_item_search_thumbnail_image_width">142dp</dimen> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
|     <dimen name="video_item_search_duration_text_size">11sp</dimen> |     <dimen name="video_item_search_duration_text_size">11sp</dimen> | ||||||
|     <dimen name="video_item_search_uploader_text_size">11sp</dimen> |     <dimen name="video_item_search_uploader_text_size">11sp</dimen> | ||||||
|     <dimen name="video_item_search_upload_date_text_size">12sp</dimen> |     <dimen name="video_item_search_upload_date_text_size">12sp</dimen> | ||||||
|  |     <dimen name="header_footer_text_size">16sp</dimen> | ||||||
|     <!-- Elements Size --> |     <!-- Elements Size --> | ||||||
|     <!-- 16 / 9 ratio--> |     <!-- 16 / 9 ratio--> | ||||||
|     <dimen name="video_item_search_thumbnail_image_width">124dp</dimen> |     <dimen name="video_item_search_thumbnail_image_width">124dp</dimen> | ||||||
|   | |||||||
| @@ -23,6 +23,16 @@ | |||||||
|     <string name="use_external_audio_player_title">Use external audio player</string> |     <string name="use_external_audio_player_title">Use external audio player</string> | ||||||
|     <string name="popup_mode_share_menu_title">NewPipe Popup mode</string> |     <string name="popup_mode_share_menu_title">NewPipe Popup mode</string> | ||||||
|     <string name="rss_button_title" translatable="false">RSS</string> |     <string name="rss_button_title" translatable="false">RSS</string> | ||||||
|  |     <string name="subscribe_button_title">Subscribe</string> | ||||||
|  |     <string name="subscribed_button_title">Subscribed</string> | ||||||
|  |     <string name="channel_unsubscribed">Channel unsubscribed</string> | ||||||
|  |     <string name="subscription_change_failed">Unable to change subscription</string> | ||||||
|  |     <string name="subscription_update_failed">Unable to update subscription</string> | ||||||
|  |  | ||||||
|  |     <string name="tab_main">Main</string> | ||||||
|  |     <string name="tab_subscriptions">Subscriptions</string> | ||||||
|  |  | ||||||
|  |     <string name="fragment_whats_new">What\'s New</string> | ||||||
|  |  | ||||||
|     <string name="controls_background_title">Background</string> |     <string name="controls_background_title">Background</string> | ||||||
|     <string name="controls_popup_title">Popup</string> |     <string name="controls_popup_title">Popup</string> | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ | |||||||
|         <item name="download">@drawable/ic_file_download_white_24dp</item> |         <item name="download">@drawable/ic_file_download_white_24dp</item> | ||||||
|         <item name="share">@drawable/ic_share_white_24dp</item> |         <item name="share">@drawable/ic_share_white_24dp</item> | ||||||
|         <item name="cast">@drawable/ic_cast_white_24dp</item> |         <item name="cast">@drawable/ic_cast_white_24dp</item> | ||||||
|         <item name="rss">@drawable/ic_rss_feed_black_24dp</item> |         <item name="rss">@drawable/ic_rss_feed_white_24dp</item> | ||||||
|         <item name="search">@drawable/ic_search_white_24dp</item> |         <item name="search">@drawable/ic_search_white_24dp</item> | ||||||
|         <item name="close">@drawable/ic_close_white_24dp</item> |         <item name="close">@drawable/ic_close_white_24dp</item> | ||||||
|         <item name="filter">@drawable/ic_filter_list_white_24dp</item> |         <item name="filter">@drawable/ic_filter_list_white_24dp</item> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tonelico
					Tonelico