mirror of
https://github.com/aculix/Channelify.git
synced 2025-12-06 06:28:15 +00:00
Initial commit
This commit is contained in:
commit
6b23bf1daa
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8" />
|
||||
</component>
|
||||
</project>
|
||||
22
.idea/gradle.xml
generated
Normal file
22
.idea/gradle.xml
generated
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="1.8" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/core" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
30
.idea/jarRepositories.xml
generated
Normal file
30
.idea/jarRepositories.xml
generated
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
Normal file
9
.idea/misc.xml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
141
app/build.gradle
Normal file
141
app/build.gradle
Normal file
@ -0,0 +1,141 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
android {
|
||||
compileSdkVersion COMPILE_SDK_VERSION
|
||||
|
||||
defaultConfig {
|
||||
applicationId "aculix.channelify.app"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion TARGET_SDK_VERSION
|
||||
versionCode 7
|
||||
versionName "1.32"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
resValue "string", "youtube_api_key", YT_API_KEY
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
resValue "string", "youtube_api_key", YT_API_KEY
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
viewBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/core_release.kotlin_module'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// Kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
|
||||
// Kotlin Coroutines
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion"
|
||||
|
||||
// Koin
|
||||
implementation "org.koin:koin-android:$koinVersion"
|
||||
implementation "org.koin:koin-androidx-viewmodel:$koinVersion"
|
||||
|
||||
// AndroidX
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.0-rc01'
|
||||
implementation "androidx.constraintlayout:constraintlayout:$clVersion"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
|
||||
// Architecture Components
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$archVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$archVersion"
|
||||
|
||||
// Room
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
|
||||
// Material theme
|
||||
implementation "com.google.android.material:material:$materialThemeVersion"
|
||||
|
||||
// Navigation
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navVersion"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navVersion"
|
||||
|
||||
// Retrofit, OkHttp & Gson
|
||||
implementation "com.squareup.retrofit2:retrofit:2.8.1"
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
|
||||
// Timber
|
||||
implementation "com.jakewharton.timber:timber:$timberVersion"
|
||||
|
||||
// Fast Adapter
|
||||
implementation "com.mikepenz:fastadapter:$fastAdapterVersion"
|
||||
implementation "com.mikepenz:fastadapter-extensions-diff:$fastAdapterVersion"
|
||||
implementation "com.mikepenz:fastadapter-extensions-utils:$fastAdapterVersion"
|
||||
implementation "com.mikepenz:fastadapter-extensions-ui:$fastAdapterVersion"
|
||||
implementation "com.mikepenz:fastadapter-extensions-paged:$fastAdapterVersion"
|
||||
implementation "com.mikepenz:itemanimators:1.1.0"
|
||||
|
||||
// Coil
|
||||
implementation "io.coil-kt:coil:0.10.0"
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime:2.1.2"
|
||||
|
||||
// TimeAgo
|
||||
implementation 'com.github.marlonlom:timeago:4.0.1'
|
||||
|
||||
// YouTube Player
|
||||
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:10.0.5'
|
||||
|
||||
// Circle ImageView
|
||||
implementation 'de.hdodenhof:circleimageview:3.1.0'
|
||||
|
||||
// Material Dialog
|
||||
implementation "com.afollestad.material-dialogs:core:$materialDialogVersion"
|
||||
implementation "com.afollestad.material-dialogs:bottomsheets:$materialDialogVersion"
|
||||
|
||||
// Kotpref
|
||||
implementation 'com.chibatching.kotpref:kotpref:2.10.0'
|
||||
|
||||
// Firebase
|
||||
implementation 'com.google.firebase:firebase-analytics:17.4.0'
|
||||
implementation 'com.google.firebase:firebase-messaging:20.1.6'
|
||||
implementation 'com.google.android.gms:play-services-ads:19.1.0'
|
||||
|
||||
// Custom Tabs
|
||||
implementation "saschpe.android:customtabs:$saschpeCustomTabs"
|
||||
implementation "androidx.browser:browser:$browserVersion"
|
||||
|
||||
// Modules
|
||||
implementation project(':core')
|
||||
}
|
||||
56
app/proguard-rules.pro
vendored
Normal file
56
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
##-------------------------- Model classes --------------------------
|
||||
-keep class aculix.channelify.app.model.** { *; }
|
||||
|
||||
|
||||
#-------------------------- Coroutines --------------------------#
|
||||
|
||||
# ServiceLoader support
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
|
||||
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
|
||||
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
|
||||
|
||||
# Most of volatile fields are updated with AFU and should not be mangled
|
||||
-keepclassmembernames class kotlinx.** {
|
||||
volatile <fields>;
|
||||
}
|
||||
|
||||
|
||||
#-------------------------- OkHttp --------------------------#
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
1
app/release/output.json
Normal file
1
app/release/output.json
Normal file
@ -0,0 +1 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":7,"versionName":"1.32","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release","dirName":""},"path":"app-release.apk","properties":{}}]
|
||||
58
app/src/main/AndroidManifest.xml
Normal file
58
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="aculix.channelify.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".Channelify"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity android:name=".activity.SplashActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustPan"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.VideoPlayerActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize|screenLayout"
|
||||
android:screenOrientation="portrait"
|
||||
/>
|
||||
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
android:resource="@array/preloaded_fonts" />
|
||||
|
||||
<!-- FCM -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_stat_notification" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_color"
|
||||
android:resource="@color/colorPrimary" />
|
||||
|
||||
<!-- Admob -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||
android:value="@string/admob_app_id"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-web.png
Normal file
BIN
app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
89
app/src/main/java/aculix/channelify/app/Channelify.kt
Normal file
89
app/src/main/java/aculix/channelify/app/Channelify.kt
Normal file
@ -0,0 +1,89 @@
|
||||
package aculix.channelify.app
|
||||
|
||||
import aculix.channelify.app.di.*
|
||||
import aculix.channelify.app.sharedpref.AppPref
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.chibatching.kotpref.Kotpref
|
||||
import com.google.android.gms.ads.MobileAds
|
||||
import com.google.android.gms.ads.RequestConfiguration
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import saschpe.android.customtabs.CustomTabsActivityLifecycleCallbacks
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
class Channelify : Application() {
|
||||
|
||||
companion object {
|
||||
var isAdEnabled = true
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isAdEnabled = resources.getBoolean(R.bool.enable_ads)
|
||||
|
||||
initializeKotpref()
|
||||
setThemeMode()
|
||||
initializeTimber()
|
||||
initializeKoin()
|
||||
initializeCustomTabs()
|
||||
|
||||
if (resources.getBoolean(R.bool.enable_ads)) initializeAdmob()
|
||||
}
|
||||
|
||||
private fun setThemeMode() {
|
||||
if (AppPref.isLightThemeEnabled) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initializeTimber() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeKotpref() {
|
||||
Kotpref.init(this)
|
||||
}
|
||||
|
||||
private fun initializeKoin() {
|
||||
startKoin {
|
||||
androidLogger()
|
||||
androidContext(this@Channelify)
|
||||
modules(
|
||||
listOf(
|
||||
appModule,
|
||||
homeModule,
|
||||
videoPlayerModule,
|
||||
commentsModule,
|
||||
videoDetailsModule,
|
||||
commentRepliesModule,
|
||||
playlistsModule,
|
||||
playlistVideosModule,
|
||||
favoritesModule,
|
||||
searchModule,
|
||||
aboutModule
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeAdmob() {
|
||||
MobileAds.initialize(this) {
|
||||
val testDeviceIds = listOf("7BD04413716C0B3DD5C73F814E02D21A")
|
||||
val configuration =
|
||||
RequestConfiguration.Builder().setTestDeviceIds(testDeviceIds).build()
|
||||
MobileAds.setRequestConfiguration(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCustomTabs() {
|
||||
registerActivityLifecycleCallbacks(CustomTabsActivityLifecycleCallbacks())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package aculix.channelify.app.activity
|
||||
|
||||
import aculix.channelify.app.Channelify
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.utils.getAdaptiveBannerAdSize
|
||||
import aculix.core.extensions.makeGone
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.gms.ads.*
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var adView: AdView
|
||||
private var initialLayoutComplete = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
|
||||
val navController = findNavController(R.id.navHostFragment)
|
||||
bottomNavView.setupWithNavController(navController)
|
||||
|
||||
if (Channelify.isAdEnabled) setupAd() else adViewContainerMain.makeGone()
|
||||
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Channelify.isAdEnabled) adView.pause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (Channelify.isAdEnabled) adView.resume()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Channelify.isAdEnabled) adView.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupAd() {
|
||||
adView = AdView(this)
|
||||
adViewContainerMain.addView(adView)
|
||||
adViewContainerMain.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
if (!initialLayoutComplete) {
|
||||
initialLayoutComplete = true
|
||||
|
||||
adView.adUnitId = getString(R.string.main_banner_ad_id)
|
||||
adView.adSize = getAdaptiveBannerAdSize(adViewContainerMain)
|
||||
adView.loadAd(AdRequest.Builder().build())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package aculix.channelify.app.activity
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package aculix.channelify.app.activity
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.fragment.VideoDetailsFragment
|
||||
import aculix.channelify.app.utils.FullScreenHelper
|
||||
import aculix.channelify.app.viewmodel.VideoPlayerViewModel
|
||||
import aculix.core.extensions.toast
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Rational
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatToggleButton
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.navigation.findNavController
|
||||
import com.google.android.gms.ads.AdListener
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.InterstitialAd
|
||||
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
|
||||
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
|
||||
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerFullScreenListener
|
||||
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.utils.loadOrCueVideo
|
||||
import kotlinx.android.synthetic.main.activity_video_player.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
|
||||
class VideoPlayerActivity : AppCompatActivity(R.layout.activity_video_player) {
|
||||
|
||||
companion object {
|
||||
const val VIDEO_ID = "video_id"
|
||||
|
||||
fun startActivity(context: Context?, videoId: String) {
|
||||
val intent = Intent(context, VideoPlayerActivity::class.java).apply {
|
||||
putExtra(VIDEO_ID, videoId)
|
||||
}
|
||||
context?.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModel<VideoPlayerViewModel>() // Lazy inject ViewModel
|
||||
|
||||
lateinit var fullScreenHelper: FullScreenHelper
|
||||
lateinit var videoId: String
|
||||
private var videoElapsedTimeInSeconds = 0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
fullScreenHelper = FullScreenHelper(this)
|
||||
videoId = intent.getStringExtra(VIDEO_ID)!!
|
||||
|
||||
// Passing the videoId as argument to the start destination
|
||||
findNavController(R.id.navHostVideoPlayer).setGraph(
|
||||
R.navigation.video_player_graph,
|
||||
bundleOf(VideoDetailsFragment.VIDEO_ID to videoId)
|
||||
)
|
||||
|
||||
initYouTubePlayer()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (ytVideoPlayerView.isFullScreen()) ytVideoPlayerView.exitFullScreen() else super.onBackPressed()
|
||||
}
|
||||
|
||||
private fun initYouTubePlayer() {
|
||||
lifecycle.addObserver(ytVideoPlayerView)
|
||||
|
||||
ytVideoPlayerView.addYouTubePlayerListener(object : AbstractYouTubePlayerListener() {
|
||||
override fun onReady(youTubePlayer: YouTubePlayer) {
|
||||
youTubePlayer.loadOrCueVideo(lifecycle, videoId, 0f)
|
||||
addFullScreenListenerToPlayer()
|
||||
setupCustomActions(youTubePlayer)
|
||||
}
|
||||
|
||||
override fun onCurrentSecond(youTubePlayer: YouTubePlayer, second: Float) {
|
||||
videoElapsedTimeInSeconds = second
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the forward and rewind action button to the Player
|
||||
*/
|
||||
private fun setupCustomActions(youTubePlayer: YouTubePlayer) {
|
||||
ytVideoPlayerView.getPlayerUiController()
|
||||
.setCustomAction1(
|
||||
ContextCompat.getDrawable(this, R.drawable.ic_rewind)!!,
|
||||
View.OnClickListener {
|
||||
videoElapsedTimeInSeconds -= 10
|
||||
youTubePlayer.seekTo(videoElapsedTimeInSeconds)
|
||||
})
|
||||
.setCustomAction2(
|
||||
ContextCompat.getDrawable(this, R.drawable.ic_forward)!!,
|
||||
View.OnClickListener {
|
||||
videoElapsedTimeInSeconds += 10
|
||||
youTubePlayer.seekTo(videoElapsedTimeInSeconds)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the orientation of the activity based on the
|
||||
* change of the player state (Full screen or not)
|
||||
*/
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun addFullScreenListenerToPlayer() {
|
||||
ytVideoPlayerView.addFullScreenListener(object : YouTubePlayerFullScreenListener {
|
||||
override fun onYouTubePlayerEnterFullScreen() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
fullScreenHelper.enterFullScreen()
|
||||
}
|
||||
|
||||
override fun onYouTubePlayerExitFullScreen() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
fullScreenHelper.exitFullScreen()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.ChannelInfo
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ChannelInfoService {
|
||||
|
||||
@GET("channels")
|
||||
suspend fun getChannelInfo(
|
||||
@Query("id") channelId: String,
|
||||
@Query("part") part: String = "snippet, statistics, brandingSettings",
|
||||
@Query("fields") fields: String = "items(snippet(title, description, publishedAt, thumbnails), statistics(viewCount, subscriberCount, videoCount), brandingSettings(image(bannerMobileHdImageUrl, bannerMobileMediumHdImageUrl)))",
|
||||
@Query("maxResults") maxResults: Int = 1
|
||||
): Response<ChannelInfo>
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.ChannelUploadsPlaylistInfo
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ChannelsService {
|
||||
|
||||
@GET("channels")
|
||||
suspend fun getChannelUploadsPlaylistInfo(
|
||||
@Query("id") channelId: String,
|
||||
@Query("part") part: String = "contentDetails"
|
||||
): Response<ChannelUploadsPlaylistInfo>
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface CommentRepliesService {
|
||||
|
||||
/**
|
||||
* Gets the list of replies for a particular comment
|
||||
*/
|
||||
@GET("comments")
|
||||
suspend fun getCommentReplies(
|
||||
@Query("parentId") commentId: String,
|
||||
@Query("pageToken") pageToken: String?,
|
||||
@Query("part") part: String = "snippet",
|
||||
@Query("fields") fields: String = "nextPageToken, items(snippet(authorDisplayName, authorProfileImageUrl, textOriginal, likeCount, publishedAt, updatedAt))",
|
||||
@Query("maxResults") maxResults: Int = Constants.YT_API_MAX_RESULTS
|
||||
): Response<CommentReply>
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface CommentService {
|
||||
|
||||
/**
|
||||
* Gets the list of comments of a particular video
|
||||
*/
|
||||
@GET("commentThreads")
|
||||
suspend fun getVideoComments(
|
||||
@Query("videoId") videoId: String,
|
||||
@Query("pageToken") pageToken: String?,
|
||||
@Query("order") sortOrder: String,
|
||||
@Query("part") part: String = "snippet",
|
||||
@Query("fields") fields: String = "nextPageToken, items(snippet(topLevelComment(id, snippet(authorDisplayName, authorProfileImageUrl, textOriginal, likeCount, publishedAt, updatedAt)), totalReplyCount))",
|
||||
@Query("maxResults") maxResults: Int = Constants.YT_API_MAX_RESULTS
|
||||
): Response<Comment>
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.ChannelUploadsPlaylistInfo
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface PlaylistItemsService {
|
||||
|
||||
@GET("playlistItems")
|
||||
suspend fun getPlaylistVideos(
|
||||
@Query("playlistId") playlistId: String,
|
||||
@Query("pageToken") pageToken: String?,
|
||||
@Query("part") part: String = "snippet,contentDetails",
|
||||
@Query("fields") fields: String = "nextPageToken, prevPageToken, items(snippet(title, thumbnails), contentDetails(videoId, videoPublishedAt))",
|
||||
@Query("maxResults") maxResults: Int = Constants.YT_API_MAX_RESULTS
|
||||
): Response<PlaylistItemInfo>
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.Playlist
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface PlaylistsService {
|
||||
|
||||
@GET("playlists")
|
||||
suspend fun getPlaylists(
|
||||
@Query("channelId") channelId: String,
|
||||
@Query("pageToken") pageToken: String?,
|
||||
@Query("part") part: String = "snippet,contentDetails",
|
||||
@Query("fields") fields: String = "nextPageToken, items(id, snippet(publishedAt, title, description, thumbnails), contentDetails)",
|
||||
@Query("maxResults") maxResults: Int = Constants.YT_API_MAX_RESULTS
|
||||
): Response<Playlist>
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.QueryMap
|
||||
|
||||
interface SearchVideoService {
|
||||
|
||||
@GET("search")
|
||||
suspend fun searchVideos(
|
||||
@Query("q") searchQuery: String,
|
||||
@Query("channelId") channelId: String,
|
||||
@Query("pageToken") pageToken: String?,
|
||||
@QueryMap defaultQueryMap: HashMap<String, String>,
|
||||
@Query("maxResults") maxResults: Int = Constants.YT_API_MAX_RESULTS
|
||||
): Response<SearchedVideo>
|
||||
}
|
||||
17
app/src/main/java/aculix/channelify/app/api/VideosService.kt
Normal file
17
app/src/main/java/aculix/channelify/app/api/VideosService.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package aculix.channelify.app.api
|
||||
|
||||
import aculix.channelify.app.model.Video
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface VideosService {
|
||||
|
||||
@GET("videos")
|
||||
suspend fun getVideoInfo(
|
||||
@Query("id") videoId: String,
|
||||
@Query("part") part: String = "snippet, statistics",
|
||||
@Query("fields") fields: String = "items(snippet(publishedAt, title, description, thumbnails), statistics)",
|
||||
@Query("maxResults") maxResults: Int = 1
|
||||
): Response<Video>
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package aculix.channelify.app.db
|
||||
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [FavoriteVideo::class], version = 1)
|
||||
abstract class ChannelifyDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun favoriteVideoDao(): FavoriteVideoDao
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package aculix.channelify.app.db
|
||||
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface FavoriteVideoDao {
|
||||
|
||||
@Insert
|
||||
suspend fun addFavoriteVideo(favoriteVideo: FavoriteVideo)
|
||||
|
||||
@Delete
|
||||
suspend fun removeFavoriteVideo(favoriteVideo: FavoriteVideo)
|
||||
|
||||
@Query("DELETE FROM favorite_videos where id in (:idList)")
|
||||
suspend fun removeMultipleFavoriteVideos(idList: List<String>)
|
||||
|
||||
@Query("SELECT id FROM favorite_videos WHERE id = :id LIMIT 1")
|
||||
suspend fun getFavoriteVideoId(id: String): String?
|
||||
|
||||
@Query("SELECT * FROM favorite_videos ORDER BY timeInMillis DESC")
|
||||
suspend fun getAllFavoriteVideos(): List<FavoriteVideo>
|
||||
|
||||
@Query("DELETE FROM favorite_videos")
|
||||
suspend fun removeAllFavoriteVideos()
|
||||
|
||||
|
||||
|
||||
}
|
||||
22
app/src/main/java/aculix/channelify/app/di/aboutModule.kt
Normal file
22
app/src/main/java/aculix/channelify/app/di/aboutModule.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.api.ChannelInfoService
|
||||
import aculix.channelify.app.repository.AboutRepository
|
||||
import aculix.channelify.app.viewmodel.AboutViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val aboutModule = module {
|
||||
|
||||
factory { provideChannelInfoService(get()) }
|
||||
|
||||
single { AboutRepository(get()) }
|
||||
|
||||
viewModel { AboutViewModel(get(), androidContext().getString(R.string.channel_id), androidContext()) }
|
||||
}
|
||||
|
||||
private fun provideChannelInfoService(retrofit: Retrofit) =
|
||||
retrofit.create(ChannelInfoService::class.java)
|
||||
120
app/src/main/java/aculix/channelify/app/di/appModule.kt
Normal file
120
app/src/main/java/aculix/channelify/app/di/appModule.kt
Normal file
@ -0,0 +1,120 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.BuildConfig
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.db.ChannelifyDatabase
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.room.Room
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import timber.log.Timber
|
||||
import java.security.MessageDigest
|
||||
|
||||
|
||||
val appModule = module {
|
||||
|
||||
single { provideApplicationSignature(androidContext()) }
|
||||
single { provideOkHttpClient(androidContext(), get()) }
|
||||
single { provideRetrofit(get()) }
|
||||
single { provideRoomDatabase(androidApplication()) }
|
||||
}
|
||||
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
fun provideApplicationSignature(context: Context): List<String> {
|
||||
val signatureList: List<String>
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val signature = context.packageManager.getPackageInfo(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
PackageManager.GET_SIGNING_CERTIFICATES
|
||||
).signingInfo
|
||||
signatureList = if (signature.hasMultipleSigners()) {
|
||||
signature.apkContentsSigners.map {
|
||||
val digest = MessageDigest.getInstance("SHA")
|
||||
digest.update(it.toByteArray())
|
||||
bytesToHex(digest.digest())
|
||||
}
|
||||
} else {
|
||||
signature.signingCertificateHistory.map {
|
||||
val digest = MessageDigest.getInstance("SHA")
|
||||
digest.update(it.toByteArray())
|
||||
bytesToHex(digest.digest())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val signature = context.packageManager.getPackageInfo(
|
||||
BuildConfig.APPLICATION_ID,
|
||||
PackageManager.GET_SIGNATURES
|
||||
).signatures
|
||||
signatureList = signature.map {
|
||||
val digest = MessageDigest.getInstance("SHA")
|
||||
digest.update(it.toByteArray())
|
||||
bytesToHex(digest.digest())
|
||||
}
|
||||
}
|
||||
return signatureList
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e.stackTrace.toString())
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun bytesToHex(bytes: ByteArray): String {
|
||||
val hexArray =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
|
||||
val hexChars = CharArray(bytes.size * 2)
|
||||
var v: Int
|
||||
for (j in bytes.indices) {
|
||||
v = bytes[j].toInt() and 0xFF
|
||||
hexChars[j * 2] = hexArray[v.ushr(4)]
|
||||
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||
}
|
||||
return String(hexChars)
|
||||
}
|
||||
|
||||
private fun provideOkHttpClient(
|
||||
androidContext: Context,
|
||||
signatureList: List<String>
|
||||
): OkHttpClient {
|
||||
val httpClient = OkHttpClient.Builder()
|
||||
|
||||
httpClient.addInterceptor(object : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val original = chain.request()
|
||||
val originalHttpUrl = original.url
|
||||
|
||||
val url = originalHttpUrl.newBuilder()
|
||||
.addQueryParameter("key", androidContext.getString(R.string.youtube_api_key))
|
||||
.build()
|
||||
|
||||
val requestBuilder = original.newBuilder().url(url)
|
||||
.addHeader("X-Android-Package", BuildConfig.APPLICATION_ID)
|
||||
.addHeader("X-Android-Cert", signatureList[0])
|
||||
val request = requestBuilder.build()
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
})
|
||||
|
||||
return httpClient.build()
|
||||
}
|
||||
|
||||
private fun provideRetrofit(httpClient: OkHttpClient) = Retrofit.Builder()
|
||||
.baseUrl("https://www.googleapis.com/youtube/v3/")
|
||||
.client(httpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
private fun provideRoomDatabase(androidApplication: Application) =
|
||||
Room.databaseBuilder(androidApplication, ChannelifyDatabase::class.java, "channelify-db")
|
||||
.build()
|
||||
@ -0,0 +1,24 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.api.CommentRepliesService
|
||||
import aculix.channelify.app.api.CommentService
|
||||
import aculix.channelify.app.repository.CommentRepliesRepository
|
||||
import aculix.channelify.app.repository.CommentsRepository
|
||||
import aculix.channelify.app.viewmodel.CommentRepliesViewModel
|
||||
import aculix.channelify.app.viewmodel.CommentsViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val commentRepliesModule = module {
|
||||
|
||||
factory { provideCommentRepliesService(get()) }
|
||||
|
||||
single { CommentRepliesRepository(get()) }
|
||||
|
||||
viewModel { CommentRepliesViewModel(get()) }
|
||||
}
|
||||
|
||||
private fun provideCommentRepliesService(retrofit: Retrofit) =
|
||||
retrofit.create(CommentRepliesService::class.java)
|
||||
21
app/src/main/java/aculix/channelify/app/di/commentsModule.kt
Normal file
21
app/src/main/java/aculix/channelify/app/di/commentsModule.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.api.CommentService
|
||||
import aculix.channelify.app.repository.CommentsRepository
|
||||
import aculix.channelify.app.viewmodel.CommentsViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val commentsModule = module {
|
||||
|
||||
factory { provideCommentService(get()) }
|
||||
|
||||
single { CommentsRepository(get()) }
|
||||
|
||||
viewModel { CommentsViewModel(get(), androidContext()) }
|
||||
}
|
||||
|
||||
private fun provideCommentService(retrofit: Retrofit) =
|
||||
retrofit.create(CommentService::class.java)
|
||||
@ -0,0 +1,15 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.repository.FavoritesRepository
|
||||
import aculix.channelify.app.viewmodel.FavoritesViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val favoritesModule = module {
|
||||
|
||||
// FavoriteVideoDao is already injected in videoDetailsModule
|
||||
|
||||
single { FavoritesRepository(get()) }
|
||||
|
||||
viewModel { FavoritesViewModel(get()) }
|
||||
}
|
||||
28
app/src/main/java/aculix/channelify/app/di/homeModule.kt
Normal file
28
app/src/main/java/aculix/channelify/app/di/homeModule.kt
Normal file
@ -0,0 +1,28 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.api.ChannelsService
|
||||
import aculix.channelify.app.api.PlaylistItemsService
|
||||
import aculix.channelify.app.api.SearchVideoService
|
||||
import aculix.channelify.app.repository.HomeRepository
|
||||
import aculix.channelify.app.viewmodel.HomeViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val homeModule = module {
|
||||
|
||||
factory { provideChannelsService(get()) }
|
||||
factory { providePlaylistItemsService(get()) }
|
||||
|
||||
single { HomeRepository(get(), get(), get()) }
|
||||
|
||||
viewModel { HomeViewModel(get(), androidContext().getString(R.string.channel_id), androidContext()) }
|
||||
}
|
||||
|
||||
private fun provideChannelsService(retrofit: Retrofit) =
|
||||
retrofit.create(ChannelsService::class.java)
|
||||
|
||||
private fun providePlaylistItemsService(retrofit: Retrofit) =
|
||||
retrofit.create(PlaylistItemsService::class.java)
|
||||
@ -0,0 +1,17 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.repository.PlaylistVideosRepository
|
||||
import aculix.channelify.app.viewmodel.PlaylistVideosViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* No need to provide PlaylistItemsService since it is already being provided by Koin in the
|
||||
* homeModule
|
||||
*/
|
||||
val playlistVideosModule = module {
|
||||
|
||||
single { PlaylistVideosRepository(get()) }
|
||||
|
||||
viewModel { PlaylistVideosViewModel(get()) }
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.api.PlaylistsService
|
||||
import aculix.channelify.app.api.SearchVideoService
|
||||
import aculix.channelify.app.repository.PlaylistsRepository
|
||||
import aculix.channelify.app.viewmodel.PlaylistsViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val playlistsModule = module {
|
||||
|
||||
factory { providePlaylistsService(get()) }
|
||||
|
||||
single { PlaylistsRepository(get()) }
|
||||
|
||||
viewModel { PlaylistsViewModel(get(), androidContext().getString(R.string.channel_id)) }
|
||||
}
|
||||
|
||||
private fun providePlaylistsService(retrofit: Retrofit) =
|
||||
retrofit.create(PlaylistsService::class.java)
|
||||
22
app/src/main/java/aculix/channelify/app/di/searchModule.kt
Normal file
22
app/src/main/java/aculix/channelify/app/di/searchModule.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.api.SearchVideoService
|
||||
import aculix.channelify.app.repository.SearchRepository
|
||||
import aculix.channelify.app.viewmodel.SearchViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val searchModule = module {
|
||||
|
||||
factory { provideSearchVideoService(get()) }
|
||||
|
||||
single { SearchRepository(get()) }
|
||||
|
||||
viewModel { SearchViewModel(get(), androidContext().getString(R.string.channel_id), androidContext().getString(R.string.error_search_empty_result_title)) }
|
||||
}
|
||||
|
||||
private fun provideSearchVideoService(retrofit: Retrofit) =
|
||||
retrofit.create(SearchVideoService::class.java)
|
||||
@ -0,0 +1,29 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.api.VideosService
|
||||
import aculix.channelify.app.db.ChannelifyDatabase
|
||||
import aculix.channelify.app.repository.VideoDetailsRepository
|
||||
import aculix.channelify.app.repository.VideoPlayerRepository
|
||||
import aculix.channelify.app.viewmodel.VideoDetailsViewModel
|
||||
import aculix.channelify.app.viewmodel.VideoPlayerViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val videoDetailsModule = module {
|
||||
factory { provideVideosService(get()) }
|
||||
|
||||
single { provideFavoriteVideoDao(get()) }
|
||||
single { VideoDetailsRepository(get(), get()) }
|
||||
|
||||
viewModel { VideoDetailsViewModel(get(), androidContext()) }
|
||||
}
|
||||
|
||||
private fun provideVideosService(retrofit: Retrofit) =
|
||||
retrofit.create(VideosService::class.java)
|
||||
|
||||
private fun provideFavoriteVideoDao(channelifyDatabase: ChannelifyDatabase) =
|
||||
channelifyDatabase.favoriteVideoDao()
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package aculix.channelify.app.di
|
||||
|
||||
import aculix.channelify.app.api.VideosService
|
||||
import aculix.channelify.app.repository.VideoPlayerRepository
|
||||
import aculix.channelify.app.viewmodel.VideoPlayerViewModel
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
val videoPlayerModule = module {
|
||||
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import aculix.core.extensions.makeGone
|
||||
import aculix.core.extensions.makeVisible
|
||||
import android.view.View
|
||||
import coil.api.load
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
import de.hdodenhof.circleimageview.CircleImageView
|
||||
|
||||
class CommentItem(val comment: Comment.Item?) : AbstractItem<CommentItem.CommentViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_comment
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_comment_item_id
|
||||
|
||||
override fun getViewHolder(v: View): CommentViewHolder {
|
||||
return CommentViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class CommentViewHolder(private var view: View) : FastAdapter.ViewHolder<CommentItem>(view) {
|
||||
|
||||
private val profilePhoto: CircleImageView = view.findViewById(R.id.ivProfileCommentItem)
|
||||
private val commenterNameTime: MaterialTextView =
|
||||
view.findViewById(R.id.tvNameTimeCommentItem)
|
||||
private val commentContent: MaterialTextView = view.findViewById(R.id.tvContentCommentItem)
|
||||
private val likeCount: MaterialTextView = view.findViewById(R.id.tvLikeCountCommentItem)
|
||||
private val repliesCount: MaterialTextView =
|
||||
view.findViewById(R.id.tvCommentCountCommentItem)
|
||||
private val viewReplies: MaterialButton = view.findViewById(R.id.btnViewRepliesCommentItem)
|
||||
|
||||
override fun bindView(item: CommentItem, payloads: List<Any>) {
|
||||
item.comment?.snippet?.let {
|
||||
// Profile photo
|
||||
profilePhoto.load(it.topLevelComment.snippet.authorProfileImageUrl)
|
||||
|
||||
// Commenter name and published time
|
||||
if(it.topLevelComment.snippet.publishedAt == it.topLevelComment.snippet.updatedAt) {
|
||||
// Comment not edited
|
||||
commenterNameTime.text = view.context.getString(
|
||||
R.string.text_commenter_name_time,
|
||||
it.topLevelComment.snippet.authorDisplayName,
|
||||
DateTimeUtils.getTimeAgo(it.topLevelComment.snippet.publishedAt)
|
||||
)
|
||||
} else {
|
||||
// Edited comment
|
||||
commenterNameTime.text = view.context.getString(
|
||||
R.string.text_commenter_name_time_edited,
|
||||
it.topLevelComment.snippet.authorDisplayName,
|
||||
DateTimeUtils.getTimeAgo(it.topLevelComment.snippet.updatedAt)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Comments count
|
||||
commentContent.text = it.topLevelComment.snippet.textOriginal
|
||||
|
||||
// Comments Like count
|
||||
likeCount.text = it.topLevelComment.snippet.likeCount.toString()
|
||||
|
||||
// Comments Reply count
|
||||
repliesCount.text = it.totalReplyCount.toString()
|
||||
|
||||
// View Replies button
|
||||
if (it.totalReplyCount > 0) {
|
||||
viewReplies.makeVisible()
|
||||
viewReplies.text = view.context.resources.getQuantityString(R.plurals.btn_view_replies, it.totalReplyCount, it.totalReplyCount)
|
||||
} else {
|
||||
viewReplies.makeGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: CommentItem) {
|
||||
profilePhoto.setImageDrawable(null)
|
||||
commenterNameTime.text = null
|
||||
commentContent.text = null
|
||||
likeCount.text = null
|
||||
repliesCount.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import android.view.View
|
||||
import coil.api.load
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
import de.hdodenhof.circleimageview.CircleImageView
|
||||
|
||||
class CommentReplyItem(val commentReply: CommentReply.Item?) :
|
||||
AbstractItem<CommentReplyItem.CommentReplyViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_comment_reply
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_comment_reply_item_id
|
||||
|
||||
override fun getViewHolder(v: View): CommentReplyViewHolder {
|
||||
return CommentReplyViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class CommentReplyViewHolder(private var view: View) :
|
||||
FastAdapter.ViewHolder<CommentReplyItem>(view) {
|
||||
|
||||
private val profilePhoto: CircleImageView =
|
||||
view.findViewById(R.id.ivProfileCommentReplyItem)
|
||||
private val commenterNameTime: MaterialTextView =
|
||||
view.findViewById(R.id.tvNameTimeCommentReplyItem)
|
||||
private val commentContent: MaterialTextView =
|
||||
view.findViewById(R.id.tvContentCommentReplyItem)
|
||||
private val likeCount: MaterialTextView =
|
||||
view.findViewById(R.id.tvLikeCountCommentReplyItem)
|
||||
|
||||
override fun bindView(item: CommentReplyItem, payloads: List<Any>) {
|
||||
item.commentReply?.snippet?.let {
|
||||
// Profile photo
|
||||
profilePhoto.load(it.authorProfileImageUrl)
|
||||
|
||||
// Commenter name and published time
|
||||
if (it.publishedAt == it.updatedAt) {
|
||||
// Comment not edited
|
||||
commenterNameTime.text = view.context.getString(
|
||||
R.string.text_commenter_name_time,
|
||||
it.authorDisplayName,
|
||||
DateTimeUtils.getTimeAgo(it.publishedAt)
|
||||
)
|
||||
} else {
|
||||
// Edited comment
|
||||
commenterNameTime.text = view.context.getString(
|
||||
R.string.text_commenter_name_time_edited,
|
||||
it.authorDisplayName,
|
||||
DateTimeUtils.getTimeAgo(it.updatedAt)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Comment Content
|
||||
commentContent.text = it.textOriginal
|
||||
|
||||
// Comments Like count
|
||||
likeCount.text = it.likeCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: CommentReplyItem) {
|
||||
profilePhoto.setImageDrawable(null)
|
||||
commenterNameTime.text = null
|
||||
commentContent.text = null
|
||||
likeCount.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import aculix.core.extensions.to64BitHash
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil.api.load
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
class FavoriteItem(val favoriteVideo: FavoriteVideo) :
|
||||
AbstractItem<FavoriteItem.FavoriteViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_favorite
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_favorite_item_id
|
||||
|
||||
override fun getViewHolder(v: View): FavoriteViewHolder {
|
||||
return FavoriteViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class FavoriteViewHolder(private var view: View) : FastAdapter.ViewHolder<FavoriteItem>(view) {
|
||||
|
||||
private val thumbnail: AppCompatImageView = view.findViewById(R.id.ivThumbnailFavoriteItem)
|
||||
private val favoriteIcon: AppCompatImageView = view.findViewById(R.id.ivHeartFavoriteItem)
|
||||
private val videoTitle: AppCompatTextView = view.findViewById(R.id.tvTitleFavoriteItem)
|
||||
|
||||
override fun bindView(item: FavoriteItem, payloads: List<Any>) {
|
||||
thumbnail.load(item.favoriteVideo.thumbnail)
|
||||
videoTitle.text = item.favoriteVideo.title
|
||||
|
||||
// Favorite Icon
|
||||
if (item.favoriteVideo.isChecked) favoriteIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
view.context,
|
||||
R.drawable.ic_favorite_filled_border
|
||||
)
|
||||
) else favoriteIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
view.context,
|
||||
R.drawable.ic_favorite_border
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun unbindView(item: FavoriteItem) {
|
||||
thumbnail.setImageDrawable(null)
|
||||
videoTitle.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import coil.api.load
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeItem(val playlistItem: PlaylistItemInfo.Item?) : AbstractItem<HomeItem.HomeViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_home
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_home_item_id
|
||||
|
||||
override fun getViewHolder(v: View): HomeViewHolder {
|
||||
return HomeViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class HomeViewHolder(private var view: View) : FastAdapter.ViewHolder<HomeItem>(view) {
|
||||
|
||||
private val thumbnail: AppCompatImageView = view.findViewById(R.id.ivThumbnailHomeItem)
|
||||
private val videoTitle: AppCompatTextView = view.findViewById(R.id.tvTitleHomeItem)
|
||||
private val videoPublishedAt: AppCompatTextView = view.findViewById(R.id.tvTimePublishedHomeItem)
|
||||
|
||||
override fun bindView(item: HomeItem, payloads: List<Any>) {
|
||||
item.playlistItem?.snippet?.let {
|
||||
thumbnail.load(it.thumbnails.standard?.url ?: it.thumbnails.high.url)
|
||||
videoTitle.text = it.title
|
||||
}
|
||||
videoPublishedAt.text = DateTimeUtils.getTimeAgo(item.playlistItem?.contentDetails?.videoPublishedAt!!)
|
||||
}
|
||||
|
||||
override fun unbindView(item: HomeItem) {
|
||||
thumbnail.setImageDrawable(null)
|
||||
videoTitle.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.Playlist
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import coil.api.load
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
class PlaylistItem(val playlistItem: Playlist.Item?) :
|
||||
AbstractItem<PlaylistItem.PlaylistViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_playlist
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_playlist_item_id
|
||||
|
||||
override fun getViewHolder(v: View): PlaylistViewHolder {
|
||||
return PlaylistViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class PlaylistViewHolder(private var view: View) : FastAdapter.ViewHolder<PlaylistItem>(view) {
|
||||
|
||||
private val thumbnail: AppCompatImageView = view.findViewById(R.id.ivThumbnailPlaylistItem)
|
||||
private val playlistName: AppCompatTextView = view.findViewById(R.id.tvNamePlaylistItem)
|
||||
private val videoCount: AppCompatTextView = view.findViewById(R.id.tvVideoCountPlaylistItem)
|
||||
|
||||
override fun bindView(item: PlaylistItem, payloads: List<Any>) {
|
||||
thumbnail.load(
|
||||
item.playlistItem?.snippet?.thumbnails?.standard?.url
|
||||
?: item.playlistItem?.snippet?.thumbnails?.high?.url
|
||||
)
|
||||
playlistName.text = item.playlistItem?.snippet?.title
|
||||
videoCount.text = view.context.resources.getQuantityString(R.plurals.text_playlist_video_count, item.playlistItem?.contentDetails?.itemCount!!, item.playlistItem.contentDetails.itemCount)
|
||||
}
|
||||
|
||||
override fun unbindView(item: PlaylistItem) {
|
||||
thumbnail.setImageDrawable(null)
|
||||
playlistName.text = null
|
||||
videoCount.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import coil.api.load
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
class PlaylistVideoItem(val playlistItem: PlaylistItemInfo.Item?) :
|
||||
AbstractItem<PlaylistVideoItem.PlaylistVideoViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_playlist_video
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_playlist_video_item_id
|
||||
|
||||
override fun getViewHolder(v: View): PlaylistVideoViewHolder {
|
||||
return PlaylistVideoViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class PlaylistVideoViewHolder(private var view: View) :
|
||||
FastAdapter.ViewHolder<PlaylistVideoItem>(view) {
|
||||
|
||||
private val thumbnail: AppCompatImageView =
|
||||
view.findViewById(R.id.ivThumbnailPlaylistVideoItem)
|
||||
private val videoTitle: AppCompatTextView = view.findViewById(R.id.tvTitlePlaylistVideoItem)
|
||||
private val videoPublishedAt: AppCompatTextView =
|
||||
view.findViewById(R.id.tvTimePublishedPlaylistVideoItem)
|
||||
|
||||
override fun bindView(item: PlaylistVideoItem, payloads: List<Any>) {
|
||||
item.playlistItem?.snippet?.let {
|
||||
thumbnail.load(it.thumbnails.standard?.url ?: it.thumbnails.high.url)
|
||||
videoTitle.text = it.title
|
||||
|
||||
}
|
||||
videoPublishedAt.text =
|
||||
DateTimeUtils.getTimeAgo(item.playlistItem?.contentDetails?.videoPublishedAt!!)
|
||||
}
|
||||
|
||||
override fun unbindView(item: PlaylistVideoItem) {
|
||||
thumbnail.setImageDrawable(null)
|
||||
videoTitle.text = null
|
||||
videoPublishedAt.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import android.view.View
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
class ProgressIndicatorItem : AbstractItem<ProgressIndicatorItem.ProgressIndicatorViewHolder>() {
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.progress_indicator_item_id
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_progress_indicator
|
||||
|
||||
override fun getViewHolder(v: View): ProgressIndicatorViewHolder {
|
||||
return ProgressIndicatorViewHolder(v)
|
||||
}
|
||||
|
||||
class ProgressIndicatorViewHolder(view: View) : FastAdapter.ViewHolder<ProgressIndicatorItem>(view) {
|
||||
|
||||
override fun bindView(item: ProgressIndicatorItem, payloads: List<Any>) {
|
||||
// No data needs to be set as only ProgressBar is shown
|
||||
}
|
||||
|
||||
override fun unbindView(item: ProgressIndicatorItem) {
|
||||
// No data set and hence no unbinding needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
package aculix.channelify.app.fastadapteritems
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import coil.api.load
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
class SearchItem(val searchedVideo: SearchedVideo.Item?) :
|
||||
AbstractItem<SearchItem.SearchViewHolder>() {
|
||||
|
||||
override val layoutRes: Int
|
||||
get() = R.layout.item_search
|
||||
|
||||
override val type: Int
|
||||
get() = R.id.fastadapter_search_item_id
|
||||
|
||||
override fun getViewHolder(v: View): SearchViewHolder {
|
||||
return SearchViewHolder(v)
|
||||
}
|
||||
|
||||
|
||||
class SearchViewHolder(private var view: View) : FastAdapter.ViewHolder<SearchItem>(view) {
|
||||
|
||||
private val thumbnail: AppCompatImageView = view.findViewById(R.id.ivThumbnailSearchItem)
|
||||
private val videoTitle: AppCompatTextView = view.findViewById(R.id.tvTitleSearchItem)
|
||||
private val videoPublishedAt: AppCompatTextView =
|
||||
view.findViewById(R.id.tvTimePublishedSearchItem)
|
||||
|
||||
override fun bindView(item: SearchItem, payloads: List<Any>) {
|
||||
item.searchedVideo?.snippet?.let {
|
||||
thumbnail.load(it.thumbnails.standard?.url ?: it.thumbnails.high.url)
|
||||
videoTitle.text = it.title
|
||||
videoPublishedAt.text = DateTimeUtils.getTimeAgo(it.publishedAt)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: SearchItem) {
|
||||
thumbnail.setImageDrawable(null)
|
||||
videoTitle.text = null
|
||||
videoPublishedAt.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.View
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.ChannelInfo
|
||||
import aculix.channelify.app.sharedpref.AppPref
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import aculix.channelify.app.viewmodel.AboutViewModel
|
||||
import aculix.core.extensions.*
|
||||
import aculix.core.helper.ResultWrapper
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import coil.api.load
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import kotlinx.android.synthetic.main.fragment_about.*
|
||||
import kotlinx.android.synthetic.main.widget_toolbar.view.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class AboutFragment : Fragment(R.layout.fragment_about) {
|
||||
|
||||
private val viewModel by viewModel<AboutViewModel>() // Lazy inject ViewModel
|
||||
|
||||
private lateinit var channelInfoItem: ChannelInfo.Item
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar()
|
||||
|
||||
setupObservables()
|
||||
fetchChannelInfo()
|
||||
|
||||
btnRetryAbout.setOnClickListener {
|
||||
fetchChannelInfo()
|
||||
}
|
||||
|
||||
btnSubscribeAbout.setOnClickListener {
|
||||
startYouTubeIntent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
ablAbout.toolbarMain.apply {
|
||||
inflateMenu(R.menu.toolbar_menu_about)
|
||||
|
||||
// Change theme menu item icon based on current theme
|
||||
val themeDrawable = if (AppPref.isLightThemeEnabled) ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_theme_light
|
||||
) else ContextCompat.getDrawable(context!!, R.drawable.ic_theme_dark)
|
||||
menu.findItem(R.id.miThemeAbout).icon = themeDrawable
|
||||
|
||||
// Store configuration
|
||||
menu.findItem(R.id.miStoreAbout).isVisible = resources.getBoolean(R.bool.enable_store)
|
||||
|
||||
// MenuItem onclick
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miStoreAbout -> {
|
||||
context.openUrl(getString(R.string.store_url), R.color.defaultBgColor)
|
||||
}
|
||||
R.id.miThemeAbout -> {
|
||||
showThemeChooserDialog()
|
||||
}
|
||||
R.id.miAppInfoAbout -> {
|
||||
findNavController().navigate(R.id.action_aboutFragment_to_appInfoFragment)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the info of channel
|
||||
*/
|
||||
private fun fetchChannelInfo() {
|
||||
if (isInternetAvailable(context!!)) {
|
||||
viewModel.getChannelInfo()
|
||||
} else {
|
||||
showChannelInfoErrorState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
viewModel.channelInfoLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
is ResultWrapper.Loading -> {
|
||||
pbAbout.makeVisible()
|
||||
}
|
||||
is ResultWrapper.Error -> {
|
||||
pbAbout.makeGone()
|
||||
showChannelInfoErrorState()
|
||||
}
|
||||
is ResultWrapper.Success<*> -> {
|
||||
pbAbout.makeGone()
|
||||
hideChannelInfoErrorState()
|
||||
channelInfoItem = (it.data as ChannelInfo).items[0]
|
||||
setChannelInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setChannelInfo() {
|
||||
|
||||
// Banner
|
||||
if (channelInfoItem.brandingSettings != null) {
|
||||
ivBannerAbout.load(
|
||||
channelInfoItem.brandingSettings?.image?.bannerMobileHdImageUrl
|
||||
?: channelInfoItem.brandingSettings?.image?.bannerMobileMediumHdImageUrl
|
||||
)
|
||||
} else {
|
||||
ivBannerAbout.makeGone()
|
||||
val constraintSet = ConstraintSet().apply {
|
||||
clone(clAbout)
|
||||
connect(R.id.cvAbout, ConstraintSet.TOP, R.id.ablAbout, ConstraintSet.BOTTOM)
|
||||
}
|
||||
constraintSet.applyTo(clAbout)
|
||||
}
|
||||
|
||||
// Profile Image
|
||||
ivProfileAbout.load(
|
||||
channelInfoItem.snippet.thumbnails.high?.url
|
||||
?: channelInfoItem.snippet.thumbnails.medium.url
|
||||
)
|
||||
|
||||
tvNameAbout.text = channelInfoItem.snippet.title
|
||||
tvJoinDateAbout.text = getString(
|
||||
R.string.text_channel_join_date,
|
||||
DateTimeUtils.getPublishedDate(channelInfoItem.snippet.publishedAt)
|
||||
)
|
||||
tvSubscribersValueAbout.text =
|
||||
channelInfoItem.statistics.subscriberCount.toLong().getFormattedNumberInString()
|
||||
tvVideosValueAbout.text =
|
||||
channelInfoItem.statistics.videoCount.toLong().getFormattedNumberInString()
|
||||
tvViewsValueAbout.text =
|
||||
channelInfoItem.statistics.viewCount.toLong().getFormattedNumberInString()
|
||||
tvDescAbout.text = channelInfoItem.snippet.description
|
||||
}
|
||||
|
||||
private fun showChannelInfoErrorState() {
|
||||
groupResultAbout.makeGone()
|
||||
groupErrorAbout.makeVisible()
|
||||
}
|
||||
|
||||
private fun hideChannelInfoErrorState() {
|
||||
groupErrorAbout.makeGone()
|
||||
groupResultAbout.makeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the YouTube app's Channel screen if YouTube app is installed otherwise opens the URL
|
||||
* in Chrome Custom Tab.
|
||||
*/
|
||||
private fun startYouTubeIntent() {
|
||||
try {
|
||||
val youtubeIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(
|
||||
getString(
|
||||
R.string.text_youtube_channel_intent_url,
|
||||
getString(R.string.channel_id)
|
||||
)
|
||||
)
|
||||
)
|
||||
startActivity(youtubeIntent)
|
||||
} catch (exception: ActivityNotFoundException) {
|
||||
context?.openUrl(
|
||||
getString(
|
||||
R.string.text_youtube_channel_intent_url,
|
||||
getString(R.string.channel_id)
|
||||
),
|
||||
R.color.defaultBgColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showThemeChooserDialog() {
|
||||
val themeList = listOf(
|
||||
getString(R.string.dialog_theme_text_light),
|
||||
getString(R.string.dialog_theme_text_dark)
|
||||
)
|
||||
|
||||
val currentThemeIndex = if (AppPref.isLightThemeEnabled) 0 else 1
|
||||
|
||||
MaterialDialog(context!!).show {
|
||||
title(R.string.dialog_theme_title)
|
||||
listItemsSingleChoice(
|
||||
items = themeList,
|
||||
initialSelection = currentThemeIndex
|
||||
) { dialog, index, text ->
|
||||
when (text) {
|
||||
getString(R.string.dialog_theme_text_light) -> {
|
||||
setTheme(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
getString(R.string.dialog_theme_text_dark) -> {
|
||||
setTheme(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTheme(themeMode: Int) {
|
||||
AppCompatDelegate.setDefaultNightMode(themeMode)
|
||||
AppPref.isLightThemeEnabled = themeMode == AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
import aculix.channelify.app.BuildConfig
|
||||
import aculix.channelify.app.R
|
||||
import aculix.core.extensions.openAppInGooglePlay
|
||||
import aculix.core.extensions.openUrl
|
||||
import aculix.core.extensions.startEmailIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import coil.api.load
|
||||
import kotlinx.android.synthetic.main.fragment_app_info.*
|
||||
|
||||
class AppInfoFragment : Fragment(R.layout.fragment_app_info) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupToolbar()
|
||||
ivLogoAppInfo.load(R.drawable.logo_splash)
|
||||
|
||||
onWebsiteClick()
|
||||
onGooglePlayClick()
|
||||
onInstagramClick()
|
||||
onEmailClick()
|
||||
onNavigationViewMenuItemClick()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
val navController = findNavController()
|
||||
val appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
toolbarAppInfo.setupWithNavController(navController, appBarConfiguration)
|
||||
}
|
||||
|
||||
private fun onWebsiteClick() {
|
||||
ivWebsiteAppInfo.setOnClickListener {
|
||||
context?.openUrl(getString(R.string.text_website_url), R.color.defaultBgColor)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGooglePlayClick() {
|
||||
ivGooglePlayAppInfo.setOnClickListener {
|
||||
try {
|
||||
// Try to open in the Google Play app
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://search?q=pub:${getString(R.string.text_google_play_developer_name)}")
|
||||
)
|
||||
)
|
||||
} catch (exception: Throwable) {
|
||||
// Google Play app is not installed. Open URL in the browser.
|
||||
context?.openUrl(
|
||||
"https://play.google.com/store/apps/dev?id=${getString(R.string.text_google_play_developer_id)}",
|
||||
R.color.defaultBgColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onInstagramClick() {
|
||||
ivInstagramAppInfo.setOnClickListener {
|
||||
try {
|
||||
// Try to open in the Instagram app
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://instagram.com/_u/${getString(R.string.text_instagram_user_name)}")
|
||||
)
|
||||
)
|
||||
} catch (exception: android.content.ActivityNotFoundException) {
|
||||
// Instagram app is not installed. Open URL in the browser.
|
||||
context?.openUrl(
|
||||
"https://instagram.com/${getString(R.string.text_instagram_user_name)}",
|
||||
R.color.defaultBgColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEmailClick() {
|
||||
ivEmailAppInfo.setOnClickListener {
|
||||
context?.startEmailIntent(getString(R.string.text_contact_email), null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNavigationViewMenuItemClick() {
|
||||
nvAppInfo.setNavigationItemSelectedListener {
|
||||
when (it.itemId) {
|
||||
R.id.miRateAppInfo -> {
|
||||
context?.openAppInGooglePlay(BuildConfig.APPLICATION_ID)
|
||||
}
|
||||
R.id.miFeedbackAppInfo -> {
|
||||
context?.startEmailIntent(
|
||||
getString(R.string.text_contact_email),
|
||||
getString(R.string.text_app_feedback_email_subject)
|
||||
)
|
||||
}
|
||||
R.id.miTosAppInfo -> {
|
||||
context?.openUrl(getString(R.string.text_tos_url), R.color.defaultBgColor)
|
||||
}
|
||||
R.id.miPrivacyPolicyAppInfo -> {
|
||||
context?.openUrl(
|
||||
getString(R.string.text_privacy_policy_url),
|
||||
R.color.defaultBgColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import aculix.channelify.app.Channelify
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.fastadapteritems.CommentReplyItem
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.utils.getAdaptiveBannerAdSize
|
||||
import aculix.channelify.app.viewmodel.CommentRepliesViewModel
|
||||
import aculix.core.extensions.makeGone
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.AdView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_comment_replies.*
|
||||
import kotlinx.android.synthetic.main.fragment_comments.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class CommentRepliesFragment : Fragment() {
|
||||
|
||||
private val viewModel by viewModel<CommentRepliesViewModel>() // Lazy inject ViewModel
|
||||
private val args by navArgs<CommentRepliesFragmentArgs>()
|
||||
|
||||
private lateinit var commentId: String
|
||||
private lateinit var commentRepliesAdapter: GenericFastAdapter
|
||||
private lateinit var commentRepliesPagedModelAdapter: PagedModelAdapter<CommentReply.Item, CommentReplyItem>
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
|
||||
private lateinit var adView: AdView
|
||||
private var initialLayoutComplete = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_comment_replies, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
commentId = args.commentId
|
||||
|
||||
setupRecyclerView(savedInstanceState)
|
||||
fetchCommentReplies()
|
||||
setupObservables()
|
||||
|
||||
ivCloseCommentReplies.setOnClickListener { onCloseClick() }
|
||||
|
||||
if (Channelify.isAdEnabled) setupAd() else adViewContainerCommentReplies.makeGone()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = commentRepliesAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
if (Channelify.isAdEnabled) adView.pause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (Channelify.isAdEnabled) adView.resume()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Channelify.isAdEnabled) adView.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<CommentReply.Item>(object :
|
||||
DiffUtil.ItemCallback<CommentReply.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: CommentReply.Item,
|
||||
newItem: CommentReply.Item
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: CommentReply.Item,
|
||||
newItem: CommentReply.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
commentRepliesPagedModelAdapter =
|
||||
PagedModelAdapter<CommentReply.Item, CommentReplyItem>(asyncDifferConfig) {
|
||||
CommentReplyItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
commentRepliesAdapter =
|
||||
FastAdapter.with(listOf(commentRepliesPagedModelAdapter, footerAdapter))
|
||||
commentRepliesAdapter.registerTypeInstance(CommentReplyItem(null))
|
||||
commentRepliesAdapter.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvCommentReplies.layoutManager = LinearLayoutManager(context)
|
||||
rvCommentReplies.addItemDecoration(
|
||||
DividerItemDecorator(ContextCompat.getDrawable(requireContext(), R.drawable.view_divider_item_decorator)!!)
|
||||
)
|
||||
rvCommentReplies.adapter = commentRepliesAdapter
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.commentRepliesLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<CommentReply.Item>> { commentRepliesList ->
|
||||
commentRepliesPagedModelAdapter.submitList(commentRepliesList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun fetchCommentReplies() {
|
||||
viewModel.getCommentReplies(commentId)
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(
|
||||
clCommentReplies,
|
||||
R.string.error_fetch_comment_replies,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
private fun setupAd() {
|
||||
adView = AdView(context)
|
||||
adViewContainerCommentReplies.addView(adView)
|
||||
adViewContainerCommentReplies.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
if (!initialLayoutComplete) {
|
||||
initialLayoutComplete = true
|
||||
|
||||
adView.adUnitId = getString(R.string.comment_replies_banner_ad_id)
|
||||
adView.adSize = activity?.getAdaptiveBannerAdSize(adViewContainerCommentReplies)
|
||||
adView.loadAd(AdRequest.Builder().build())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,272 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
import aculix.channelify.app.Channelify
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.fastadapteritems.CommentItem
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.utils.getAdaptiveBannerAdSize
|
||||
import aculix.channelify.app.viewmodel.CommentsViewModel
|
||||
import aculix.core.extensions.makeGone
|
||||
import aculix.core.extensions.makeVisible
|
||||
import aculix.core.extensions.toast
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.*
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.AdSize
|
||||
import com.google.android.gms.ads.AdView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.listeners.ClickEventHook
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_comments.*
|
||||
import kotlinx.android.synthetic.main.item_comment.view.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import timber.log.Timber
|
||||
class CommentsFragment : Fragment() {
|
||||
|
||||
private val viewModel by viewModel<CommentsViewModel>() // Lazy inject ViewModel
|
||||
private val args by navArgs<CommentsFragmentArgs>()
|
||||
|
||||
private lateinit var videoId: String
|
||||
private lateinit var commentsAdapter: GenericFastAdapter
|
||||
private lateinit var commentsPagedModelAdapter: PagedModelAdapter<Comment.Item, CommentItem>
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
private val SORT_BY_RELEVANCE = "relevance"
|
||||
private val SORT_BY_TIME = "time"
|
||||
|
||||
private lateinit var adView: AdView
|
||||
private var initialLayoutComplete = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false)
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
videoId = args.videoId
|
||||
|
||||
setupRecyclerView(savedInstanceState)
|
||||
fetchComments(SORT_BY_RELEVANCE)
|
||||
setupObservables()
|
||||
|
||||
ivSortComments.setOnClickListener { onSortClick(it) }
|
||||
ivCloseComments.setOnClickListener { onCloseClick() }
|
||||
|
||||
if (Channelify.isAdEnabled) setupAd() else adViewContainerComments.makeGone()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = commentsAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
if (Channelify.isAdEnabled) adView.pause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (Channelify.isAdEnabled) adView.resume()
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Channelify.isAdEnabled) adView.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<Comment.Item>(object :
|
||||
DiffUtil.ItemCallback<Comment.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Comment.Item,
|
||||
newItem: Comment.Item
|
||||
): Boolean {
|
||||
return oldItem.snippet.topLevelComment.id == newItem.snippet.topLevelComment.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Comment.Item,
|
||||
newItem: Comment.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
commentsPagedModelAdapter =
|
||||
PagedModelAdapter<Comment.Item, CommentItem>(asyncDifferConfig) {
|
||||
CommentItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
commentsAdapter = FastAdapter.with(listOf(commentsPagedModelAdapter, footerAdapter))
|
||||
commentsAdapter.registerTypeInstance(CommentItem(null))
|
||||
commentsAdapter.withSavedInstanceState(savedInstanceState)
|
||||
onViewRepliesClick()
|
||||
|
||||
rvComments.layoutManager = LinearLayoutManager(context)
|
||||
rvComments.addItemDecoration(DividerItemDecorator(ContextCompat.getDrawable(requireContext(), R.drawable.view_divider_item_decorator)!!))
|
||||
rvComments.adapter = commentsAdapter
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
// Observe Empty State LiveData
|
||||
viewModel.emptyStateLiveData.observe(viewLifecycleOwner, Observer { isResultEmpty ->
|
||||
if (isResultEmpty) {
|
||||
showEmptyState()
|
||||
} else {
|
||||
hideEmptyState()
|
||||
}
|
||||
})
|
||||
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.commentsLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<Comment.Item>> { commentsList ->
|
||||
commentsPagedModelAdapter.submitList(commentsList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
groupEmptyComments.makeVisible()
|
||||
}
|
||||
|
||||
private fun hideEmptyState() {
|
||||
groupEmptyComments.makeGone()
|
||||
}
|
||||
|
||||
private fun fetchComments(sortOrder: String) {
|
||||
viewModel.getVideoComments(videoId, sortOrder)
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(clComments, R.string.error_fetch_comments, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Sort icon is clicked
|
||||
*/
|
||||
private fun onSortClick(view: View) {
|
||||
val sortMenu = PopupMenu(requireContext(), view)
|
||||
sortMenu.menuInflater.inflate(R.menu.comments_sort_menu_video_details, sortMenu.menu)
|
||||
sortMenu.show()
|
||||
|
||||
sortMenu.setOnMenuItemClickListener { item: MenuItem? ->
|
||||
when (item?.itemId) {
|
||||
R.id.miTopComments -> {
|
||||
viewModel.sortComments(SORT_BY_RELEVANCE)
|
||||
}
|
||||
R.id.miNewestFirstComments -> {
|
||||
viewModel.sortComments(SORT_BY_TIME)
|
||||
Timber.e("Newest Comments")
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun onCloseClick() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the View Replies button is clicked of a
|
||||
* Comment Item
|
||||
*/
|
||||
private fun onViewRepliesClick() {
|
||||
commentsAdapter.addEventHook(object : ClickEventHook<CommentItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
|
||||
return if (viewHolder is CommentItem.CommentViewHolder) {
|
||||
viewHolder.itemView.btnViewRepliesCommentItem
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(
|
||||
v: View,
|
||||
position: Int,
|
||||
fastAdapter: FastAdapter<CommentItem>,
|
||||
item: CommentItem
|
||||
) {
|
||||
val action =
|
||||
CommentsFragmentDirections.actionCommentsFragmentToCommentRepliesFragment(item.comment?.snippet?.topLevelComment?.id!!)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupAd() {
|
||||
adView = AdView(context)
|
||||
adViewContainerComments.addView(adView)
|
||||
adViewContainerComments.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
if (!initialLayoutComplete) {
|
||||
initialLayoutComplete = true
|
||||
|
||||
adView.adUnitId = getString(R.string.comments_banner_ad_id)
|
||||
adView.adSize = activity?.getAdaptiveBannerAdSize(adViewContainerComments)
|
||||
adView.loadAd(AdRequest.Builder().build())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,159 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.View
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.activity.VideoPlayerActivity
|
||||
import aculix.channelify.app.fastadapteritems.FavoriteItem
|
||||
import aculix.channelify.app.viewmodel.FavoritesViewModel
|
||||
import aculix.core.extensions.openUrl
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.FastItemAdapter
|
||||
import com.mikepenz.fastadapter.listeners.ClickEventHook
|
||||
import com.mikepenz.itemanimators.AlphaInAnimator
|
||||
import kotlinx.android.synthetic.main.fragment_favorites.*
|
||||
import kotlinx.android.synthetic.main.item_favorite.view.*
|
||||
import kotlinx.android.synthetic.main.widget_toolbar.view.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
*/
|
||||
class FavoritesFragment : Fragment(R.layout.fragment_favorites) {
|
||||
|
||||
private val viewModel by viewModel<FavoritesViewModel>() // Lazy inject ViewModel
|
||||
|
||||
private lateinit var favoritesAdapter: FastItemAdapter<FavoriteItem>
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar()
|
||||
|
||||
setupRecyclerView(savedInstanceState)
|
||||
setupObservables()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = favoritesAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
ablFavorites.toolbarMain.apply {
|
||||
inflateMenu(R.menu.main_menu)
|
||||
|
||||
// Store and Search configuration
|
||||
menu.findItem(R.id.miStoreMainMenu).isVisible = resources.getBoolean(R.bool.enable_store)
|
||||
menu.findItem(R.id.miSearchMainMenu).isVisible = resources.getBoolean(R.bool.enable_search)
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miStoreMainMenu -> {
|
||||
context.openUrl(getString(R.string.store_url), R.color.defaultBgColor)
|
||||
}
|
||||
R.id.miSearchMainMenu -> {
|
||||
findNavController().navigate(R.id.action_favoritesFragment_to_searchFragment)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
favoritesAdapter = FastItemAdapter()
|
||||
favoritesAdapter.setHasStableIds(true)
|
||||
favoritesAdapter.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvFavorites.layoutManager = LinearLayoutManager(context)
|
||||
rvFavorites.itemAnimator = AlphaInAnimator()
|
||||
rvFavorites.adapter = favoritesAdapter
|
||||
rvFavorites.itemAnimator = AlphaInAnimator()
|
||||
|
||||
onFavoriteClick()
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
viewModel.favoriteVideosLiveData.observe(viewLifecycleOwner, Observer { favoriteVideoList ->
|
||||
val favoriteItemsList = ArrayList<FavoriteItem>()
|
||||
for (favoriteVideo in favoriteVideoList) {
|
||||
favoriteItemsList.add(FavoriteItem(favoriteVideo))
|
||||
}
|
||||
|
||||
favoritesAdapter.add(favoriteItemsList)
|
||||
showEmptyState(favoritesAdapter.itemCount)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showEmptyState(itemCount: Int) {
|
||||
groupEmptyFavorites.isVisible = itemCount < 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Heart Icon is clicked of a RecyclerView Item
|
||||
*/
|
||||
private fun onFavoriteClick() {
|
||||
favoritesAdapter.addEventHook(object : ClickEventHook<FavoriteItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
|
||||
return if (viewHolder is FavoriteItem.FavoriteViewHolder) {
|
||||
viewHolder.itemView.ivHeartFavoriteItem
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(
|
||||
v: View,
|
||||
position: Int,
|
||||
fastAdapter: FastAdapter<FavoriteItem>,
|
||||
item: FavoriteItem
|
||||
) {
|
||||
val favoriteIcon = v as AppCompatImageView
|
||||
|
||||
if (item.favoriteVideo.isChecked) {
|
||||
// Icon unchecked
|
||||
item.favoriteVideo.isChecked = false
|
||||
favoriteIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_favorite_border
|
||||
)
|
||||
)
|
||||
viewModel.removeVideoFromFavorites(item.favoriteVideo)
|
||||
} else {
|
||||
// Icon checked
|
||||
item.favoriteVideo.isChecked = true
|
||||
favoriteIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
context!!,
|
||||
R.drawable.ic_favorite_filled_border
|
||||
)
|
||||
)
|
||||
viewModel.addVideoToFavorites(item.favoriteVideo)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the RecyclerView is clicked
|
||||
*/
|
||||
private fun onItemClick() {
|
||||
favoritesAdapter.onClickListener = { view, adapter, item, position ->
|
||||
VideoPlayerActivity.startActivity(context, item.favoriteVideo.id)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
247
app/src/main/java/aculix/channelify/app/fragment/HomeFragment.kt
Normal file
247
app/src/main/java/aculix/channelify/app/fragment/HomeFragment.kt
Normal file
@ -0,0 +1,247 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.activity.VideoPlayerActivity
|
||||
import aculix.channelify.app.fastadapteritems.HomeItem
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.viewmodel.HomeViewModel
|
||||
import aculix.core.extensions.*
|
||||
import aculix.core.helper.ResultWrapper
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.ads.AdListener
|
||||
import com.google.android.gms.ads.AdRequest
|
||||
import com.google.android.gms.ads.InterstitialAd
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.paged.ExperimentalPagedSupport
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.widget_toolbar.view.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@ExperimentalPagedSupport
|
||||
class HomeFragment : Fragment(R.layout.fragment_home) {
|
||||
|
||||
private val viewModel by viewModel<HomeViewModel>() // Lazy inject ViewModel
|
||||
|
||||
private var homeAdapter: GenericFastAdapter? = null
|
||||
private lateinit var homePagedModelAdapter: PagedModelAdapter<PlaylistItemInfo.Item, HomeItem>
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar()
|
||||
|
||||
if (requireContext().isInternetAvailable()) {
|
||||
viewModel.getLatestVideos()
|
||||
} else {
|
||||
showErrorState()
|
||||
}
|
||||
|
||||
setupUploadsPlaylistIdObservables()
|
||||
setupRecyclerView(savedInstanceState)
|
||||
onRetryButtonClick()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
homeAdapter?.let {
|
||||
var outState = _outState
|
||||
outState = it.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
ablHome.toolbarMain.apply {
|
||||
inflateMenu(R.menu.main_menu)
|
||||
|
||||
// Store and Search configuration
|
||||
menu.findItem(R.id.miStoreMainMenu).isVisible =
|
||||
resources.getBoolean(R.bool.enable_store)
|
||||
menu.findItem(R.id.miSearchMainMenu).isVisible =
|
||||
resources.getBoolean(R.bool.enable_search)
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miStoreMainMenu -> {
|
||||
context.openUrl(getString(R.string.store_url), R.color.defaultBgColor)
|
||||
}
|
||||
R.id.miSearchMainMenu -> {
|
||||
findNavController().navigate(R.id.action_homeFragment_to_searchFragment)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<PlaylistItemInfo.Item>(object :
|
||||
DiffUtil.ItemCallback<PlaylistItemInfo.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: PlaylistItemInfo.Item,
|
||||
newItem: PlaylistItemInfo.Item
|
||||
): Boolean {
|
||||
return oldItem.contentDetails.videoId == newItem.contentDetails.videoId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: PlaylistItemInfo.Item,
|
||||
newItem: PlaylistItemInfo.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
homePagedModelAdapter =
|
||||
PagedModelAdapter<PlaylistItemInfo.Item, HomeItem>(asyncDifferConfig) {
|
||||
HomeItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
homeAdapter = FastAdapter.with(listOf(homePagedModelAdapter, footerAdapter))
|
||||
homeAdapter?.registerTypeInstance(HomeItem(null))
|
||||
homeAdapter?.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvHome.layoutManager = LinearLayoutManager(context)
|
||||
rvHome.adapter = homeAdapter
|
||||
rvHome.addItemDecoration(
|
||||
DividerItemDecorator(
|
||||
ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.view_divider_item_decorator
|
||||
)!!
|
||||
)
|
||||
)
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
private fun setupUploadsPlaylistIdObservables() {
|
||||
viewModel.uploadsPlaylistIdLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
is ResultWrapper.Loading -> {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
is ResultWrapper.Error -> {
|
||||
// Error occurred while fetching the uploads playlist id
|
||||
showErrorState(it.errorMessage)
|
||||
}
|
||||
is ResultWrapper.Success<*> -> {
|
||||
// Success in fetching the uploads playlist id
|
||||
hideErrorState()
|
||||
setupLatestVideosObservables()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupLatestVideosObservables() {
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
pbHome.makeGone()
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
pbHome.makeGone()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.latestVideoLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<PlaylistItemInfo.Item>> { latestVideoList ->
|
||||
homePagedModelAdapter.submitList(latestVideoList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun showErrorState(errorMsg: String = getString(R.string.error_internet_connectivity)) {
|
||||
rvHome.makeGone()
|
||||
pbHome.makeGone()
|
||||
groupErrorHome.makeVisible()
|
||||
tvErrorHome.text = errorMsg
|
||||
}
|
||||
|
||||
private fun hideErrorState() {
|
||||
groupErrorHome.makeGone()
|
||||
rvHome.makeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Retry button of the error state is clicked
|
||||
*/
|
||||
private fun onRetryButtonClick() {
|
||||
btnRetryHome.setOnClickListener {
|
||||
if (requireContext().isInternetAvailable()) viewModel.getLatestVideos()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the RecyclerView is clicked
|
||||
*/
|
||||
private fun onItemClick() {
|
||||
homeAdapter?.onClickListener = { view, adapter, item, position ->
|
||||
if (item is HomeItem) {
|
||||
VideoPlayerActivity.startActivity(
|
||||
context,
|
||||
item.playlistItem?.contentDetails?.videoId!!
|
||||
)
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(clHome, R.string.error_load_more_videos, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAnchorView(activity?.findViewById(R.id.bottomNavView) as BottomNavigationView)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,249 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.activity.VideoPlayerActivity
|
||||
import aculix.channelify.app.fastadapteritems.HomeItem
|
||||
import aculix.channelify.app.fastadapteritems.PlaylistVideoItem
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.viewmodel.HomeViewModel
|
||||
import aculix.channelify.app.viewmodel.PlaylistVideosViewModel
|
||||
import aculix.core.extensions.makeGone
|
||||
import aculix.core.extensions.startShareTextIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import coil.api.load
|
||||
import com.afollestad.materialdialogs.LayoutMode
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.customview.getCustomView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.fragment_playlist_details.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_playlist_videos.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass to show the list of videos of a particular playlist.
|
||||
*/
|
||||
class PlaylistVideosFragment : Fragment() {
|
||||
|
||||
private val viewModel by viewModel<PlaylistVideosViewModel>() // Lazy inject ViewModel
|
||||
private val args by navArgs<PlaylistVideosFragmentArgs>()
|
||||
|
||||
private lateinit var playlistVideosAdapter: GenericFastAdapter
|
||||
private lateinit var playlistVideosPagedModelAdapter: PagedModelAdapter<PlaylistItemInfo.Item, PlaylistVideoItem>
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
private lateinit var playlistId: String
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_playlist_videos, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
playlistId = args.playlistId
|
||||
setupToolbar()
|
||||
|
||||
setupRecyclerView(savedInstanceState)
|
||||
fetchPlaylistVideos()
|
||||
setupObservables()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = playlistVideosAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
val navController = findNavController()
|
||||
val appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
toolbarPlaylistVideos.setupWithNavController(navController, appBarConfiguration)
|
||||
|
||||
// Inflate Menu
|
||||
toolbarPlaylistVideos.inflateMenu(R.menu.toolbar_menu_playlist_videos)
|
||||
toolbarPlaylistVideos.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miDetailsPlaylistVideos -> {
|
||||
showPlaylistDetails()
|
||||
}
|
||||
R.id.miSharePlaylistVideos -> {
|
||||
sharePlaylistUrl()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<PlaylistItemInfo.Item>(object :
|
||||
DiffUtil.ItemCallback<PlaylistItemInfo.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: PlaylistItemInfo.Item,
|
||||
newItem: PlaylistItemInfo.Item
|
||||
): Boolean {
|
||||
return oldItem.contentDetails.videoId == newItem.contentDetails.videoId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: PlaylistItemInfo.Item,
|
||||
newItem: PlaylistItemInfo.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
playlistVideosPagedModelAdapter =
|
||||
PagedModelAdapter<PlaylistItemInfo.Item, PlaylistVideoItem>(asyncDifferConfig) {
|
||||
PlaylistVideoItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
playlistVideosAdapter =
|
||||
FastAdapter.with(listOf(playlistVideosPagedModelAdapter, footerAdapter))
|
||||
playlistVideosAdapter.registerTypeInstance(PlaylistVideoItem(null))
|
||||
playlistVideosAdapter.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvPlaylistVideos.layoutManager = LinearLayoutManager(context)
|
||||
rvPlaylistVideos.adapter = playlistVideosAdapter
|
||||
rvPlaylistVideos.addItemDecoration(
|
||||
DividerItemDecorator(ContextCompat.getDrawable(requireContext(), R.drawable.view_divider_item_decorator)!!)
|
||||
)
|
||||
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
private fun fetchPlaylistVideos() {
|
||||
viewModel.getPlaylistVideos(playlistId)
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.playlistVideosLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<PlaylistItemInfo.Item>> { playlistVideosList ->
|
||||
playlistVideosPagedModelAdapter.submitList(playlistVideosList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(
|
||||
clPlaylistVideos,
|
||||
R.string.error_load_more_videos,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAnchorView(activity?.findViewById(R.id.bottomNavView) as BottomNavigationView)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the RecyclerView is clicked
|
||||
*/
|
||||
private fun onItemClick() {
|
||||
playlistVideosAdapter.onClickListener = { view, adapter, item, position ->
|
||||
if (item is PlaylistVideoItem) {
|
||||
VideoPlayerActivity.startActivity(context, item.playlistItem?.contentDetails?.videoId!!)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlaylistDetails() {
|
||||
val playlistDetailsDialog =
|
||||
MaterialDialog(requireContext(), BottomSheet(LayoutMode.WRAP_CONTENT)).show {
|
||||
customView(R.layout.fragment_playlist_details, scrollable = true)
|
||||
|
||||
}
|
||||
|
||||
playlistDetailsDialog.getCustomView().apply {
|
||||
ivThumbnailPlaylistDetails.load(args.playlistThumbUrl)
|
||||
tvNamePlaylistDetails.text = args.playlistName
|
||||
tvVideoCountPlaylistDetails.text = context.resources.getQuantityString(
|
||||
R.plurals.text_playlist_video_count,
|
||||
args.playlistVideoCount.toInt(),
|
||||
args.playlistVideoCount.toInt()
|
||||
)
|
||||
tvTimePublishedPlaylistDetails.text = context.getString(
|
||||
R.string.text_playlist_published_date,
|
||||
DateTimeUtils.getPublishedDate(args.playlistPublishedTime)
|
||||
)
|
||||
tvDescPlaylistDetails.text = args.playlistDesc
|
||||
}
|
||||
}
|
||||
|
||||
private fun sharePlaylistUrl() {
|
||||
context?.startShareTextIntent(
|
||||
getString(R.string.text_share_playlist),
|
||||
getString(R.string.text_playlist_share_url, args.playlistId)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.fastadapteritems.PlaylistItem
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.model.Playlist
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.viewmodel.PlaylistsViewModel
|
||||
import aculix.core.extensions.makeGone
|
||||
import aculix.core.extensions.makeVisible
|
||||
import aculix.core.extensions.openUrl
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_playlists.*
|
||||
import kotlinx.android.synthetic.main.widget_toolbar.view.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass to show the list of
|
||||
* Playlists of a channel.
|
||||
*/
|
||||
class PlaylistsFragment : Fragment(R.layout.fragment_playlists) {
|
||||
|
||||
private val viewModel by viewModel<PlaylistsViewModel>() // Lazy inject ViewModel
|
||||
|
||||
private lateinit var playlistsAdapter: GenericFastAdapter
|
||||
private lateinit var playlistsPagedModelAdapter: PagedModelAdapter<Playlist.Item, PlaylistItem>
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar()
|
||||
|
||||
setupRecyclerView(savedInstanceState)
|
||||
fetchPlaylists()
|
||||
setupObservables()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = playlistsAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
ablPlaylists.toolbarMain.apply {
|
||||
inflateMenu(R.menu.main_menu)
|
||||
|
||||
// Store and Search configuration
|
||||
menu.findItem(R.id.miStoreMainMenu).isVisible =
|
||||
resources.getBoolean(R.bool.enable_store)
|
||||
menu.findItem(R.id.miSearchMainMenu).isVisible =
|
||||
resources.getBoolean(R.bool.enable_search)
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miStoreMainMenu -> {
|
||||
context.openUrl(getString(R.string.store_url), R.color.defaultBgColor)
|
||||
}
|
||||
R.id.miSearchMainMenu -> {
|
||||
findNavController().navigate(R.id.action_playlistsFragment_to_searchFragment)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<Playlist.Item>(object :
|
||||
DiffUtil.ItemCallback<Playlist.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Playlist.Item,
|
||||
newItem: Playlist.Item
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Playlist.Item,
|
||||
newItem: Playlist.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
playlistsPagedModelAdapter =
|
||||
PagedModelAdapter<Playlist.Item, PlaylistItem>(asyncDifferConfig) {
|
||||
PlaylistItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
playlistsAdapter = FastAdapter.with(listOf(playlistsPagedModelAdapter, footerAdapter))
|
||||
playlistsAdapter.registerTypeInstance(PlaylistItem(null))
|
||||
playlistsAdapter.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvPlaylists.layoutManager = LinearLayoutManager(context)
|
||||
rvPlaylists.addItemDecoration(
|
||||
DividerItemDecorator(
|
||||
ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.view_divider_item_decorator
|
||||
)!!
|
||||
)
|
||||
)
|
||||
rvPlaylists.adapter = playlistsAdapter
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
// Observe Empty State LiveData
|
||||
viewModel.emptyStateLiveData.observe(viewLifecycleOwner, Observer { isResultEmpty ->
|
||||
if (isResultEmpty) {
|
||||
showEmptyState()
|
||||
} else {
|
||||
hideEmptyState()
|
||||
}
|
||||
})
|
||||
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
pbPlaylists.makeGone()
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
pbPlaylists.makeGone()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.playlistsLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<Playlist.Item>> { playlistsList ->
|
||||
playlistsPagedModelAdapter.submitList(playlistsList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
groupEmptyPlaylists.makeVisible()
|
||||
}
|
||||
|
||||
private fun hideEmptyState() {
|
||||
groupEmptyPlaylists.makeGone()
|
||||
}
|
||||
|
||||
private fun fetchPlaylists() {
|
||||
viewModel.getPlaylists()
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(clPlaylists, R.string.error_fetch_playlists, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAnchorView(activity?.findViewById(R.id.bottomNavView) as BottomNavigationView)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick() {
|
||||
playlistsAdapter.onClickListener = { view, adapter, item, position ->
|
||||
if (item is PlaylistItem) {
|
||||
val action =
|
||||
PlaylistsFragmentDirections.actionPlaylistsFragmentToPlaylistVideosFragment(
|
||||
item.playlistItem?.snippet?.title!!,
|
||||
item.playlistItem.id,
|
||||
item.playlistItem.snippet.description,
|
||||
item.playlistItem.contentDetails.itemCount.toFloat(),
|
||||
item.playlistItem.snippet.thumbnails.standard?.url
|
||||
?: item.playlistItem.snippet.thumbnails.high.url,
|
||||
item.playlistItem.snippet.publishedAt
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.activity.VideoPlayerActivity
|
||||
import aculix.channelify.app.fastadapteritems.ProgressIndicatorItem
|
||||
import aculix.channelify.app.fastadapteritems.SearchItem
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.paging.Status
|
||||
import aculix.channelify.app.utils.DividerItemDecorator
|
||||
import aculix.channelify.app.viewmodel.SearchViewModel
|
||||
import aculix.core.extensions.dismissKeyboard
|
||||
import aculix.core.extensions.makeGone
|
||||
import aculix.core.extensions.makeVisible
|
||||
import aculix.core.extensions.showKeyboard
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.GenericFastAdapter
|
||||
import com.mikepenz.fastadapter.adapters.GenericItemAdapter
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter
|
||||
import com.mikepenz.fastadapter.paged.PagedModelAdapter
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
private val viewModel by viewModel<SearchViewModel>() // Lazy inject ViewModel
|
||||
|
||||
private lateinit var searchAdapter: GenericFastAdapter
|
||||
private lateinit var searchPagedModelAdapter: PagedModelAdapter<SearchedVideo.Item, SearchItem>
|
||||
private lateinit var footerAdapter: GenericItemAdapter
|
||||
private var isFirstPageLoading = true
|
||||
private var retrySnackbar: Snackbar? = null
|
||||
private var isSearchRequestInitialized = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupToolbar()
|
||||
|
||||
setupSearchView()
|
||||
setupRecyclerView(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(_outState: Bundle) {
|
||||
var outState = _outState
|
||||
outState = searchAdapter.saveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
svSearch.dismissKeyboard(context)
|
||||
retrySnackbar?.dismiss() // Dismiss the retrySnackbar if already present
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
val navController = findNavController()
|
||||
val appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
toolbarSearch.setupWithNavController(navController, appBarConfiguration)
|
||||
}
|
||||
|
||||
private fun setupSearchView() {
|
||||
svSearch.apply {
|
||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
svSearch.dismissKeyboard(context)
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
pbSearch.makeVisible()
|
||||
|
||||
if (!isSearchRequestInitialized) {
|
||||
isSearchRequestInitialized = true
|
||||
viewModel.searchVideos(query)
|
||||
setupObservables() // A bug arises and create() of DataSourceFactory is not called if observables are set before making an initial call :|
|
||||
} else {
|
||||
viewModel.setSearchQuery(query)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(p0: String?): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// Set focus on the SearchView and open the keyboard
|
||||
setOnQueryTextFocusChangeListener { view, hasFocus ->
|
||||
if (hasFocus) {
|
||||
svSearch.findFocus().showKeyboard(context)
|
||||
}
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView(savedInstanceState: Bundle?) {
|
||||
val asyncDifferConfig = AsyncDifferConfig.Builder<SearchedVideo.Item>(object :
|
||||
DiffUtil.ItemCallback<SearchedVideo.Item>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: SearchedVideo.Item,
|
||||
newItem: SearchedVideo.Item
|
||||
): Boolean {
|
||||
return oldItem.id.videoId == newItem.id.videoId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: SearchedVideo.Item,
|
||||
newItem: SearchedVideo.Item
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}).build()
|
||||
|
||||
searchPagedModelAdapter =
|
||||
PagedModelAdapter<SearchedVideo.Item, SearchItem>(asyncDifferConfig) {
|
||||
SearchItem(it)
|
||||
}
|
||||
|
||||
footerAdapter = ItemAdapter.items()
|
||||
|
||||
searchAdapter = FastAdapter.with(listOf(searchPagedModelAdapter, footerAdapter))
|
||||
searchAdapter.registerTypeInstance(SearchItem(null))
|
||||
searchAdapter.withSavedInstanceState(savedInstanceState)
|
||||
|
||||
rvSearch.layoutManager = LinearLayoutManager(context)
|
||||
rvSearch.addItemDecoration(
|
||||
DividerItemDecorator(
|
||||
ContextCompat.getDrawable(
|
||||
requireContext(),
|
||||
R.drawable.view_divider_item_decorator
|
||||
)!!
|
||||
)
|
||||
)
|
||||
rvSearch.adapter = searchAdapter
|
||||
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
// Observe Empty State LiveData
|
||||
viewModel.emptyStateLiveData.observe(viewLifecycleOwner, Observer { isResultEmpty ->
|
||||
if (isResultEmpty) {
|
||||
// True as no pages are loaded. If not done two loaders are shown when searched again.
|
||||
isFirstPageLoading = true
|
||||
showEmptyState()
|
||||
} else {
|
||||
hideEmptyState()
|
||||
}
|
||||
})
|
||||
|
||||
// Observe network live data
|
||||
viewModel.networkStateLiveData?.observe(viewLifecycleOwner, Observer { networkState ->
|
||||
when (networkState?.status) {
|
||||
Status.FAILED -> {
|
||||
footerAdapter.clear()
|
||||
pbSearch.makeGone()
|
||||
createRetrySnackbar()
|
||||
retrySnackbar?.show()
|
||||
}
|
||||
|
||||
Status.SUCCESS -> {
|
||||
footerAdapter.clear()
|
||||
pbSearch.makeGone()
|
||||
hideEmptyState()
|
||||
}
|
||||
|
||||
Status.LOADING -> {
|
||||
if (!isFirstPageLoading) {
|
||||
showRecyclerViewProgressIndicator()
|
||||
} else {
|
||||
isFirstPageLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Observe latest video live data
|
||||
viewModel.searchResultLiveData?.observe(
|
||||
viewLifecycleOwner,
|
||||
Observer<PagedList<SearchedVideo.Item>> { videoList ->
|
||||
searchPagedModelAdapter.submitList(videoList)
|
||||
})
|
||||
}
|
||||
|
||||
private fun showRecyclerViewProgressIndicator() {
|
||||
footerAdapter.clear()
|
||||
val progressIndicatorItem = ProgressIndicatorItem()
|
||||
footerAdapter.add(progressIndicatorItem)
|
||||
}
|
||||
|
||||
private fun createRetrySnackbar() {
|
||||
retrySnackbar =
|
||||
Snackbar.make(clSearch, R.string.error_load_more_videos, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAnchorView(activity?.findViewById(R.id.bottomNavView) as BottomNavigationView)
|
||||
.setAction(R.string.btn_retry) {
|
||||
viewModel.refreshFailedRequest()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
groupEmptySearch.makeVisible()
|
||||
}
|
||||
|
||||
private fun hideEmptyState() {
|
||||
groupEmptySearch.makeGone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the RecyclerView is clicked
|
||||
*/
|
||||
private fun onItemClick() {
|
||||
searchAdapter.onClickListener = { view, adapter, item, position ->
|
||||
if (item is SearchItem) {
|
||||
VideoPlayerActivity.startActivity(context, item.searchedVideo?.id?.videoId!!)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
package aculix.channelify.app.fragment
|
||||
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import aculix.channelify.app.model.Video
|
||||
import aculix.channelify.app.utils.DateTimeUtils
|
||||
import aculix.channelify.app.viewmodel.VideoDetailsViewModel
|
||||
import aculix.core.extensions.*
|
||||
import aculix.core.helper.ResultWrapper
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.android.synthetic.main.fragment_video_details.*
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class VideoDetailsFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
const val VIDEO_ID = "videoId"
|
||||
}
|
||||
|
||||
private val viewModel by viewModel<VideoDetailsViewModel>() // Lazy inject ViewModel
|
||||
private val args by navArgs<VideoDetailsFragmentArgs>()
|
||||
|
||||
private lateinit var videoId: String
|
||||
private lateinit var videoItem: Video.Item
|
||||
private var isVideoAddedToFavorite = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_details, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setupObservables()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
videoId = args.videoId
|
||||
|
||||
fetchVideoInfo()
|
||||
fetchVideoFavoriteStatus()
|
||||
|
||||
setupBottomAppBar()
|
||||
tvCommentsVideoDetails.setOnClickListener { onCommentsClick() }
|
||||
btnRetryVideoDetails.setOnClickListener { onRetryClick() }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches the info of video
|
||||
*/
|
||||
private fun fetchVideoInfo() {
|
||||
if (isInternetAvailable(requireContext())) {
|
||||
viewModel.getVideoInfo(videoId)
|
||||
} else {
|
||||
showVideoInfoErrorState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current playing video is already added to favorites or not
|
||||
*/
|
||||
private fun fetchVideoFavoriteStatus() {
|
||||
viewModel.getVideoFavoriteStatus(videoId)
|
||||
}
|
||||
|
||||
private fun setupObservables() {
|
||||
viewModel.videoInfoLiveData.observe(viewLifecycleOwner, Observer {
|
||||
when (it) {
|
||||
is ResultWrapper.Error -> {
|
||||
showVideoInfoErrorState()
|
||||
}
|
||||
is ResultWrapper.Success<*> -> {
|
||||
hideVideoInfoErrorState()
|
||||
videoItem = (it.data as Video).items[0]
|
||||
setVideoInfo()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.favoriteVideoLiveData.observe(viewLifecycleOwner, Observer { isFavorite ->
|
||||
isVideoAddedToFavorite = isFavorite
|
||||
|
||||
if (isVideoAddedToFavorite) {
|
||||
babVideoDetails.menu.findItem(R.id.miFavoriteBabVideoDetails)
|
||||
.setIcon(R.drawable.ic_favorite_filled)
|
||||
} else {
|
||||
babVideoDetails.menu.findItem(R.id.miFavoriteBabVideoDetails)
|
||||
.setIcon(R.drawable.ic_favorite_border)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setVideoInfo() {
|
||||
with(videoItem) {
|
||||
tvVideoTitleVideoDetails.text = snippet.title
|
||||
tvViewCountVideoDetails.text =
|
||||
statistics.viewCount?.toLong()?.getFormattedNumberInString() ?: getString(
|
||||
R.string.text_count_unavailable
|
||||
)
|
||||
tvLikeCountVideoDetails.text =
|
||||
statistics.likeCount?.toLong()?.getFormattedNumberInString() ?: getString(
|
||||
R.string.text_count_unavailable
|
||||
)
|
||||
tvDislikeCountVideoDetails.text =
|
||||
statistics.dislikeCount?.toLong()?.getFormattedNumberInString() ?: getString(
|
||||
R.string.text_count_unavailable
|
||||
)
|
||||
tvCommentCountVideoDetails.text =
|
||||
statistics.commentCount.toLong().getFormattedNumberInString()
|
||||
tvVideoDescVideoDetails.text = getString(
|
||||
R.string.text_video_description,
|
||||
DateTimeUtils.getPublishedDate(snippet.publishedAt),
|
||||
snippet.description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVideoInfoErrorState() {
|
||||
groupInfoVideoDetails.makeGone()
|
||||
groupErrorVideoDetails.makeVisible()
|
||||
}
|
||||
|
||||
private fun hideVideoInfoErrorState() {
|
||||
groupErrorVideoDetails.makeGone()
|
||||
groupInfoVideoDetails.makeVisible()
|
||||
}
|
||||
|
||||
|
||||
private fun setupBottomAppBar() {
|
||||
babVideoDetails.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.miFavoriteBabVideoDetails -> {
|
||||
if (this::videoItem.isInitialized) {
|
||||
// Add or remove from favorites only after videoItem details are fetched
|
||||
isVideoAddedToFavorite = if (isVideoAddedToFavorite) {
|
||||
item.setIcon(R.drawable.ic_favorite_border)
|
||||
removeVideoFromFavorites()
|
||||
false
|
||||
} else {
|
||||
// Add video to favorites
|
||||
item.setIcon(R.drawable.ic_favorite_filled)
|
||||
addVideoToFavorites()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
context?.toast(getString(R.string.text_fetch_video_details_wait_msg))
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.miShareBabVideoDetails -> {
|
||||
shareVideoUrl()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun addVideoToFavorites() {
|
||||
val favoriteVideo = FavoriteVideo(
|
||||
videoId,
|
||||
videoItem.snippet.title,
|
||||
videoItem.snippet.thumbnails.standard?.url ?: videoItem.snippet.thumbnails.high.url,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
viewModel.addVideoToFavorites(favoriteVideo)
|
||||
}
|
||||
|
||||
private fun removeVideoFromFavorites() {
|
||||
val favoriteVideo = FavoriteVideo(
|
||||
videoId,
|
||||
videoItem.snippet.title,
|
||||
videoItem.snippet.thumbnails.standard?.url ?: videoItem.snippet.thumbnails.high.url,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
viewModel.removeVideoFromFavorites(favoriteVideo)
|
||||
}
|
||||
|
||||
private fun shareVideoUrl() {
|
||||
context?.startShareTextIntent(
|
||||
getString(R.string.text_share_video),
|
||||
getString(R.string.text_video_share_url, videoId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCommentsClick() {
|
||||
findNavController().navigate(
|
||||
VideoDetailsFragmentDirections.actionVideoDetailsFragmentToCommentsFragment(
|
||||
videoId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRetryClick() {
|
||||
fetchVideoInfo()
|
||||
}
|
||||
}
|
||||
58
app/src/main/java/aculix/channelify/app/model/ChannelInfo.kt
Normal file
58
app/src/main/java/aculix/channelify/app/model/ChannelInfo.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
data class ChannelInfo(
|
||||
val items: List<Item>
|
||||
) {
|
||||
data class Item(
|
||||
val brandingSettings: BrandingSettings?,
|
||||
val snippet: Snippet,
|
||||
val statistics: Statistics
|
||||
) {
|
||||
data class BrandingSettings(
|
||||
val image: Image?
|
||||
) {
|
||||
data class Image(
|
||||
val bannerMobileHdImageUrl: String?,
|
||||
val bannerMobileMediumHdImageUrl: String
|
||||
)
|
||||
}
|
||||
|
||||
data class Snippet(
|
||||
val description: String,
|
||||
val publishedAt: String,
|
||||
val thumbnails: Thumbnails,
|
||||
val title: String
|
||||
) {
|
||||
data class Thumbnails(
|
||||
val default: Default,
|
||||
val high: High?,
|
||||
val medium: Medium
|
||||
) {
|
||||
data class Default(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class High(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Medium(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Statistics(
|
||||
val subscriberCount: String,
|
||||
val videoCount: String,
|
||||
val viewCount: String
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Model class that is returned when an API call is made to
|
||||
* request details about the Uploads playlist of the channel
|
||||
*
|
||||
* URL: https://www.googleapis.com/youtube/v3/channels
|
||||
*/
|
||||
data class ChannelUploadsPlaylistInfo(
|
||||
val items: List<Item>
|
||||
) {
|
||||
data class Item(
|
||||
val contentDetails: ContentDetails
|
||||
) {
|
||||
data class ContentDetails(
|
||||
val relatedPlaylists: RelatedPlaylists
|
||||
) {
|
||||
data class RelatedPlaylists(
|
||||
val uploads: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/src/main/java/aculix/channelify/app/model/Comment.kt
Normal file
32
app/src/main/java/aculix/channelify/app/model/Comment.kt
Normal file
@ -0,0 +1,32 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Comment(
|
||||
val items: List<Item>,
|
||||
val nextPageToken: String?
|
||||
) {
|
||||
data class Item(
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class Snippet(
|
||||
val topLevelComment: TopLevelComment,
|
||||
val totalReplyCount: Int
|
||||
) {
|
||||
data class TopLevelComment(
|
||||
val id: String,
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class Snippet(
|
||||
val authorDisplayName: String,
|
||||
val authorProfileImageUrl: String,
|
||||
val likeCount: Int,
|
||||
val publishedAt: String,
|
||||
val textOriginal: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class CommentReply(
|
||||
val items: List<Item>,
|
||||
val nextPageToken: String?
|
||||
) {
|
||||
data class Item(
|
||||
val id: String,
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class Snippet(
|
||||
val authorDisplayName: String,
|
||||
val authorProfileImageUrl: String,
|
||||
val likeCount: Int,
|
||||
val publishedAt: String,
|
||||
val textOriginal: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "favorite_videos")
|
||||
data class FavoriteVideo(@PrimaryKey val id: String,
|
||||
val title: String,
|
||||
val thumbnail: String,
|
||||
val timeInMillis: Long,
|
||||
var isChecked: Boolean)
|
||||
57
app/src/main/java/aculix/channelify/app/model/Playlist.kt
Normal file
57
app/src/main/java/aculix/channelify/app/model/Playlist.kt
Normal file
@ -0,0 +1,57 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Playlist(
|
||||
val items: List<Item>,
|
||||
val nextPageToken: String
|
||||
) {
|
||||
data class Item(
|
||||
val contentDetails: ContentDetails,
|
||||
val id: String,
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class ContentDetails(
|
||||
val itemCount: Int
|
||||
)
|
||||
|
||||
data class Snippet(
|
||||
val description: String,
|
||||
val publishedAt: String,
|
||||
val thumbnails: Thumbnails,
|
||||
val title: String
|
||||
) {
|
||||
data class Thumbnails(
|
||||
val default: Default,
|
||||
val high: High,
|
||||
val medium: Medium,
|
||||
val standard: Standard?
|
||||
) {
|
||||
data class Default(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class High(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Medium(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Standard(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PlaylistItemInfo(
|
||||
val nextPageToken: String?,
|
||||
val prevPageToken: String?,
|
||||
val items: List<Item>
|
||||
) {
|
||||
data class Item(
|
||||
val contentDetails: ContentDetails,
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class ContentDetails(
|
||||
val videoId: String,
|
||||
val videoPublishedAt: String
|
||||
)
|
||||
|
||||
data class Snippet(
|
||||
val thumbnails: Thumbnails,
|
||||
val title: String
|
||||
) {
|
||||
data class Thumbnails(
|
||||
val default: Default,
|
||||
val high: High,
|
||||
val maxres: Maxres,
|
||||
val medium: Medium,
|
||||
val standard: Standard?
|
||||
) {
|
||||
data class Default(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class High(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Maxres(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Medium(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Standard(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
/**
|
||||
* Model class that is returned when a Video Search API call is made.
|
||||
*
|
||||
* URL: https://www.googleapis.com/youtube/v3/search
|
||||
*/
|
||||
data class SearchedVideo(
|
||||
val items: List<Item>,
|
||||
val nextPageToken: String?
|
||||
) {
|
||||
data class Item(
|
||||
val id: Id,
|
||||
val snippet: Snippet
|
||||
) {
|
||||
data class Id(
|
||||
val videoId: String
|
||||
)
|
||||
|
||||
data class Snippet(
|
||||
val publishedAt: String,
|
||||
val thumbnails: Thumbnails,
|
||||
val title: String
|
||||
) {
|
||||
data class Thumbnails(
|
||||
val default: Default,
|
||||
val high: High,
|
||||
val medium: Medium,
|
||||
val standard: Standard?
|
||||
|
||||
) {
|
||||
data class Default(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class High(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Medium(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Standard(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
app/src/main/java/aculix/channelify/app/model/Video.kt
Normal file
67
app/src/main/java/aculix/channelify/app/model/Video.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package aculix.channelify.app.model
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Video(
|
||||
val items: List<Item>
|
||||
) {
|
||||
data class Item(
|
||||
val snippet: Snippet,
|
||||
val statistics: Statistics
|
||||
) {
|
||||
|
||||
data class Snippet(
|
||||
val description: String,
|
||||
val publishedAt: String,
|
||||
val title: String,
|
||||
val thumbnails: Thumbnails
|
||||
) {
|
||||
data class Thumbnails(
|
||||
val default: Default,
|
||||
val high: High,
|
||||
val maxres: Maxres,
|
||||
val medium: Medium,
|
||||
val standard: Standard?
|
||||
) {
|
||||
data class Default(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class High(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Maxres(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Medium(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
|
||||
data class Standard(
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val width: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Statistics(
|
||||
val commentCount: String,
|
||||
val dislikeCount: String?,
|
||||
val favoriteCount: String,
|
||||
val likeCount: String?,
|
||||
val viewCount: String?
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package aculix.channelify.app.paging
|
||||
|
||||
/**
|
||||
* Class used to handle network state
|
||||
*/
|
||||
data class NetworkState constructor(val status: Status,
|
||||
val msg: String? = null) {
|
||||
|
||||
companion object {
|
||||
val LOADED = NetworkState(Status.SUCCESS)
|
||||
val LOADING = NetworkState(Status.LOADING)
|
||||
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.CommentRepliesRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class CommentRepliesDataSource(
|
||||
private val commentRepliesRepository: CommentRepliesRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val commentId: String
|
||||
) : PageKeyedDataSource<String, CommentReply.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? = null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, CommentReply.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, CommentReply.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, CommentReply.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<CommentReply.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val commentReply = commentRepliesRepository.getCommentReplies(commentId, nextPageToken).body()
|
||||
nextPageToken = commentReply?.nextPageToken
|
||||
val commentRepliesList = commentReply?.items
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(commentRepliesList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.CommentsRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class CommentsDataSource(
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val videoId: String,
|
||||
var sortOrder: String
|
||||
) : PageKeyedDataSource<String, Comment.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? = null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, Comment.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, Comment.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, Comment.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<Comment.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val comment = commentsRepository.getVideoComments(videoId, nextPageToken, sortOrder).body()
|
||||
nextPageToken = comment?.nextPageToken
|
||||
val commentsList = comment?.items
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(commentsList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.HomeRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeDataSource(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val playlistId: String
|
||||
) : PageKeyedDataSource<String, PlaylistItemInfo.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? = null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<PlaylistItemInfo.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val playlistItemInfo = homeRepository.getLatestVideos(playlistId, nextPageToken).body()
|
||||
nextPageToken = playlistItemInfo?.nextPageToken
|
||||
val latestVideosList = playlistItemInfo?.items
|
||||
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(latestVideosList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.PlaylistVideosRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class PlaylistVideosDataSource(
|
||||
private val repository: PlaylistVideosRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val playlistId: String
|
||||
) : PageKeyedDataSource<String, PlaylistItemInfo.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? = null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, PlaylistItemInfo.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<PlaylistItemInfo.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val playlistItemInfo = repository.getPlaylistVideos(playlistId, nextPageToken).body()
|
||||
nextPageToken = playlistItemInfo?.nextPageToken
|
||||
val playlistVideosList = playlistItemInfo?.items
|
||||
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(playlistVideosList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.Playlist
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.PlaylistsRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class PlaylistsDataSource(
|
||||
private val repository: PlaylistsRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val channelId: String
|
||||
) : PageKeyedDataSource<String, Playlist.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? = null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, Playlist.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, Playlist.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, Playlist.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<Playlist.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val response = repository.getPlaylists(channelId, nextPageToken).body()
|
||||
nextPageToken = response?.nextPageToken
|
||||
val playlistList = response?.items
|
||||
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(playlistList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package aculix.channelify.app.paging.datasource
|
||||
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.SearchRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
|
||||
class SearchDataSource(
|
||||
private val searchRepository: SearchRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val channelId: String,
|
||||
private var searchQuery: String,
|
||||
private val emptySearchResultText: String
|
||||
) : PageKeyedDataSource<String, SearchedVideo.Item>() {
|
||||
|
||||
private var supervisorJob = SupervisorJob()
|
||||
private val networkState = MutableLiveData<NetworkState>()
|
||||
private var retryQuery: (() -> Any)? =
|
||||
null // Keep reference of the last query (to be able to retry it if necessary)
|
||||
private var nextPageToken: String? = null
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<String, SearchedVideo.Item>
|
||||
) {
|
||||
retryQuery = { loadInitial(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, null, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadAfter(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, SearchedVideo.Item>
|
||||
) {
|
||||
retryQuery = { loadAfter(params, callback) }
|
||||
executeQuery {
|
||||
callback.onResult(it, nextPageToken)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBefore(
|
||||
params: LoadParams<String>,
|
||||
callback: LoadCallback<String, SearchedVideo.Item>
|
||||
) {
|
||||
// Data is always fetched from the next page and hence loadBefore is never needed
|
||||
}
|
||||
|
||||
private fun executeQuery(callback: (List<SearchedVideo.Item>) -> Unit) {
|
||||
networkState.postValue(NetworkState.LOADING)
|
||||
coroutineScope.launch(getJobErrorHandler() + supervisorJob) {
|
||||
val searchVideoResult =
|
||||
searchRepository.searchVideos(searchQuery, channelId, nextPageToken).body()
|
||||
nextPageToken = searchVideoResult?.nextPageToken
|
||||
val videoList = searchVideoResult?.items
|
||||
retryQuery = null
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
|
||||
callback(videoList ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
|
||||
Timber.e("An error happened: $e")
|
||||
networkState.postValue(
|
||||
NetworkState.error(
|
||||
e.localizedMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
super.invalidate()
|
||||
supervisorJob.cancelChildren() // Cancel possible running job to only keep last result searched by user
|
||||
}
|
||||
|
||||
fun getNetworkState(): LiveData<NetworkState> = networkState
|
||||
|
||||
fun refresh() = this.invalidate()
|
||||
|
||||
fun retryFailedQuery() {
|
||||
val prevQuery = retryQuery
|
||||
retryQuery = null
|
||||
prevQuery?.invoke()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.paging.datasource.CommentRepliesDataSource
|
||||
import aculix.channelify.app.repository.CommentRepliesRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CommentRepliesDataSourceFactory(
|
||||
private val commentRepliesRepository: CommentRepliesRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val commentId: String
|
||||
) : DataSource.Factory<String, CommentReply.Item>() {
|
||||
|
||||
private var commentRepliesDataSource: CommentRepliesDataSource? = null
|
||||
val commentRepliesDataSourceLiveData = MutableLiveData<CommentRepliesDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, CommentReply.Item> {
|
||||
// Also called every time when invalidate() is executed
|
||||
commentRepliesDataSource =
|
||||
CommentRepliesDataSource(
|
||||
commentRepliesRepository,
|
||||
coroutineScope,
|
||||
commentId
|
||||
)
|
||||
commentRepliesDataSourceLiveData.postValue(commentRepliesDataSource)
|
||||
|
||||
return commentRepliesDataSource!!
|
||||
}
|
||||
|
||||
fun getSource() = commentRepliesDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.paging.datasource.CommentsDataSource
|
||||
import aculix.channelify.app.repository.CommentsRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
class CommentsDataSourceFactory(
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val videoId: String,
|
||||
private var sortOrder: String
|
||||
) : DataSource.Factory<String, Comment.Item>() {
|
||||
|
||||
private var commentsDataSource: CommentsDataSource? = null
|
||||
val commentsDataSourceLiveData = MutableLiveData<CommentsDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, Comment.Item> {
|
||||
// Also called every time when invalidate() is executed
|
||||
commentsDataSource =
|
||||
CommentsDataSource(
|
||||
commentsRepository,
|
||||
coroutineScope,
|
||||
videoId,
|
||||
sortOrder
|
||||
)
|
||||
commentsDataSourceLiveData.postValue(commentsDataSource)
|
||||
|
||||
return commentsDataSource!!
|
||||
}
|
||||
|
||||
fun setSortOrder(updatedSortOrder: String) {
|
||||
sortOrder = updatedSortOrder
|
||||
}
|
||||
|
||||
fun getSource() = commentsDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.datasource.HomeDataSource
|
||||
import aculix.channelify.app.repository.HomeRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class HomeDataSourceFactory(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val playlistId: String
|
||||
) : DataSource.Factory<String, PlaylistItemInfo.Item>() {
|
||||
|
||||
private var homeDataSource: HomeDataSource? = null
|
||||
val homeDataSourceLiveData = MutableLiveData<HomeDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, PlaylistItemInfo.Item> {
|
||||
if (homeDataSource == null) {
|
||||
homeDataSource =
|
||||
HomeDataSource(
|
||||
homeRepository,
|
||||
coroutineScope,
|
||||
playlistId
|
||||
)
|
||||
homeDataSourceLiveData.postValue(homeDataSource)
|
||||
}
|
||||
return homeDataSource!!
|
||||
}
|
||||
|
||||
fun getSource() = homeDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.datasource.PlaylistVideosDataSource
|
||||
import aculix.channelify.app.repository.PlaylistVideosRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PlaylistVideosDataSourceFactory(
|
||||
private val playlistVideosRepository: PlaylistVideosRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val playlistId: String
|
||||
) : DataSource.Factory<String, PlaylistItemInfo.Item>() {
|
||||
|
||||
private var playlistVideosDataSource: PlaylistVideosDataSource? = null
|
||||
val playlistVideosDataSourceLiveData = MutableLiveData<PlaylistVideosDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, PlaylistItemInfo.Item> {
|
||||
if (playlistVideosDataSource == null) {
|
||||
playlistVideosDataSource =
|
||||
PlaylistVideosDataSource(
|
||||
playlistVideosRepository,
|
||||
coroutineScope,
|
||||
playlistId
|
||||
)
|
||||
playlistVideosDataSourceLiveData.postValue(playlistVideosDataSource)
|
||||
}
|
||||
return playlistVideosDataSource!!
|
||||
}
|
||||
|
||||
fun getSource() = playlistVideosDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.Playlist
|
||||
import aculix.channelify.app.paging.datasource.PlaylistsDataSource
|
||||
import aculix.channelify.app.repository.PlaylistsRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PlaylistsDataSourceFactory(
|
||||
private val playlistRepository: PlaylistsRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val channelId: String
|
||||
) : DataSource.Factory<String, Playlist.Item>() {
|
||||
|
||||
private var playlistsDataSource: PlaylistsDataSource? = null
|
||||
val playlistsDataSourceLiveData = MutableLiveData<PlaylistsDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, Playlist.Item> {
|
||||
if (playlistsDataSource == null) {
|
||||
playlistsDataSource =
|
||||
PlaylistsDataSource(
|
||||
playlistRepository,
|
||||
coroutineScope,
|
||||
channelId
|
||||
)
|
||||
playlistsDataSourceLiveData.postValue(playlistsDataSource)
|
||||
}
|
||||
return playlistsDataSource!!
|
||||
}
|
||||
|
||||
fun getSource() = playlistsDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package aculix.channelify.app.paging.datasourcefactory
|
||||
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.paging.datasource.SearchDataSource
|
||||
import aculix.channelify.app.repository.SearchRepository
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
class SearchDataSourceFactory(
|
||||
private val searchRepository: SearchRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val channelId: String,
|
||||
private var searchQuery: String,
|
||||
private val emptySearchResultText: String
|
||||
) : DataSource.Factory<String, SearchedVideo.Item>() {
|
||||
|
||||
private var searchDataSource: SearchDataSource? = null
|
||||
val searchDataSourceLiveData = MutableLiveData<SearchDataSource>()
|
||||
|
||||
override fun create(): DataSource<String, SearchedVideo.Item> {
|
||||
// Also called every time when invalidate() is executed
|
||||
searchDataSource =
|
||||
SearchDataSource(
|
||||
searchRepository,
|
||||
coroutineScope,
|
||||
channelId,
|
||||
searchQuery,
|
||||
emptySearchResultText
|
||||
)
|
||||
searchDataSourceLiveData.postValue(searchDataSource)
|
||||
|
||||
return searchDataSource!!
|
||||
}
|
||||
|
||||
fun setSearchQuery(updatedSearchQuery: String) {
|
||||
searchQuery = updatedSearchQuery
|
||||
}
|
||||
|
||||
fun getSource() = searchDataSourceLiveData.value
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.ChannelInfoService
|
||||
|
||||
class AboutRepository(private val channelInfoService: ChannelInfoService) {
|
||||
|
||||
suspend fun getChannelInfo(channelId: String) =
|
||||
channelInfoService.getChannelInfo(channelId)
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.CommentRepliesService
|
||||
import aculix.channelify.app.api.CommentService
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import retrofit2.Response
|
||||
|
||||
class CommentRepliesRepository(private val commentRepliesService: CommentRepliesService) {
|
||||
|
||||
suspend fun getCommentReplies(commentId: String, pageToken: String?): Response<CommentReply> =
|
||||
commentRepliesService.getCommentReplies(commentId, pageToken)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.CommentService
|
||||
import aculix.channelify.app.model.Comment
|
||||
import retrofit2.Response
|
||||
|
||||
class CommentsRepository(private val commentService: CommentService) {
|
||||
|
||||
suspend fun getVideoComments(videoId: String, pageToken: String?, sortOrder: String): Response<Comment> =
|
||||
commentService.getVideoComments(videoId, pageToken, sortOrder)
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.db.FavoriteVideoDao
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import androidx.lifecycle.LiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FavoritesRepository(private val favoriteVideoDao: FavoriteVideoDao) {
|
||||
|
||||
suspend fun getFavoriteVideosFromDb(): List<FavoriteVideo> = withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.getAllFavoriteVideos()
|
||||
}
|
||||
|
||||
suspend fun removeVideoFromFavorites(favoriteVideo: FavoriteVideo) {
|
||||
withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.removeFavoriteVideo(favoriteVideo)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addVideoToFavorites(favoriteVideo: FavoriteVideo) {
|
||||
withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.addFavoriteVideo(favoriteVideo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.ChannelsService
|
||||
import aculix.channelify.app.api.PlaylistItemsService
|
||||
import aculix.channelify.app.api.SearchVideoService
|
||||
import aculix.channelify.app.model.ChannelUploadsPlaylistInfo
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import retrofit2.Response
|
||||
|
||||
class HomeRepository(private val searchVideoService: SearchVideoService,
|
||||
private val channelsService: ChannelsService,
|
||||
private val playlistItemsService: PlaylistItemsService) {
|
||||
|
||||
suspend fun getUploadsPlaylistId(channelId: String): Response<ChannelUploadsPlaylistInfo> =
|
||||
channelsService.getChannelUploadsPlaylistInfo(channelId)
|
||||
|
||||
suspend fun getLatestVideos(playlistId: String, pageToken: String?) =
|
||||
playlistItemsService.getPlaylistVideos(playlistId, pageToken)
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.PlaylistItemsService
|
||||
|
||||
class PlaylistVideosRepository(private val playlistItemsService: PlaylistItemsService) {
|
||||
|
||||
suspend fun getPlaylistVideos(playlistId: String, pageToken: String?) =
|
||||
playlistItemsService.getPlaylistVideos(playlistId, pageToken)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.PlaylistsService
|
||||
|
||||
class PlaylistsRepository(private val playlistsService: PlaylistsService) {
|
||||
|
||||
suspend fun getPlaylists(channelId: String, pageToken: String?) =
|
||||
playlistsService.getPlaylists(channelId, pageToken)
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.SearchVideoService
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchRepository(private val searchVideoService: SearchVideoService) {
|
||||
|
||||
suspend fun searchVideos(searchQuery: String, channelId: String, pageToken: String?) = withContext(Dispatchers.IO) {
|
||||
val defaultQueryMap = HashMap<String, String>()
|
||||
defaultQueryMap.apply {
|
||||
put("part", "id,snippet")
|
||||
put("fields", "nextPageToken, items(id(videoId), snippet(publishedAt, thumbnails, title))")
|
||||
put("order", "relevance")
|
||||
put("type", "video")
|
||||
}
|
||||
|
||||
searchVideoService.searchVideos(searchQuery, channelId, pageToken, defaultQueryMap)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.VideosService
|
||||
import aculix.channelify.app.db.FavoriteVideoDao
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import aculix.channelify.app.model.Video
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.Response
|
||||
|
||||
class VideoDetailsRepository(
|
||||
private val videosService: VideosService,
|
||||
private val favoriteVideoDao: FavoriteVideoDao
|
||||
) {
|
||||
|
||||
suspend fun getVideoInfo(videoId: String): Response<Video> =
|
||||
videosService.getVideoInfo(videoId)
|
||||
|
||||
suspend fun addVideoToFavorites(favoriteVideo: FavoriteVideo) {
|
||||
withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.addFavoriteVideo(favoriteVideo)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeVideoFromFavorites(favoriteVideo: FavoriteVideo) {
|
||||
withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.removeFavoriteVideo(favoriteVideo)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isVideoAddedToFavorites(videoId: String): Boolean = withContext(Dispatchers.IO) {
|
||||
favoriteVideoDao.getFavoriteVideoId(videoId) != null
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package aculix.channelify.app.repository
|
||||
|
||||
import aculix.channelify.app.api.VideosService
|
||||
import aculix.channelify.app.model.Video
|
||||
import retrofit2.Response
|
||||
|
||||
class VideoPlayerRepository() {
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package aculix.channelify.app.sharedpref
|
||||
|
||||
import com.chibatching.kotpref.KotprefModel
|
||||
|
||||
object AppPref : KotprefModel() {
|
||||
var isLightThemeEnabled by booleanPref(true)
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
import android.util.DisplayMetrics
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.gms.ads.AdSize
|
||||
|
||||
/**
|
||||
* Returns the size of the Adaptive Banner Ad based on the screen width
|
||||
*/
|
||||
fun AppCompatActivity.getAdaptiveBannerAdSize(adViewContainer: FrameLayout): AdSize {
|
||||
val display = windowManager.defaultDisplay
|
||||
val outMetrics = DisplayMetrics()
|
||||
display.getMetrics(outMetrics)
|
||||
|
||||
val density = outMetrics.density
|
||||
|
||||
var adWidthPixels = adViewContainer.width.toFloat()
|
||||
if (adWidthPixels == 0f) {
|
||||
adWidthPixels = outMetrics.widthPixels.toFloat()
|
||||
}
|
||||
|
||||
val adWidth = (adWidthPixels / density).toInt()
|
||||
return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(this, adWidth)
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
object Constants {
|
||||
const val INITIAL_PAGE_LOAD_SIZE = 10
|
||||
const val PAGE_SIZE = 10
|
||||
|
||||
const val YT_API_MAX_RESULTS = 10
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.github.marlonlom.utilities.timeago.TimeAgo
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object DateTimeUtils {
|
||||
|
||||
/**
|
||||
* Returns time in ago format
|
||||
* Eg. 14 hours ago
|
||||
* Eg. 2 days ago
|
||||
*/
|
||||
fun getTimeAgo(timeInIso8601: String): String {
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val timeInMillis = sdf.parse(timeInIso8601).time
|
||||
return TimeAgo.using(timeInMillis)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data in format MMM dd, yyyy
|
||||
* Eg. Dec 02, 2019
|
||||
*/
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun getPublishedDate(timeInIso8601: String): String {
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val date = sdf.parse(timeInIso8601)
|
||||
|
||||
val publishedDateSdf = SimpleDateFormat("MMM dd, yyyy ")
|
||||
return publishedDateSdf.format(date)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
|
||||
class DividerItemDecorator(private val mDivider: Drawable) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val dividerLeft = parent.paddingLeft
|
||||
val dividerRight = parent.width - parent.paddingRight
|
||||
|
||||
val childCount = parent.childCount
|
||||
for (i in 0..childCount - 2) {
|
||||
val child = parent.getChildAt(i)
|
||||
val params =
|
||||
child.layoutParams as RecyclerView.LayoutParams
|
||||
val dividerTop = child.bottom + params.bottomMargin
|
||||
val dividerBottom = dividerTop + mDivider.intrinsicHeight
|
||||
mDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom)
|
||||
mDivider.draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
import android.util.DisplayMetrics
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.gms.ads.AdSize
|
||||
|
||||
/**
|
||||
* Returns the size of the Adaptive Banner Ad based on the screen width
|
||||
*/
|
||||
fun FragmentActivity.getAdaptiveBannerAdSize(adViewContainer: FrameLayout): AdSize {
|
||||
val display = windowManager.defaultDisplay
|
||||
val outMetrics = DisplayMetrics()
|
||||
display.getMetrics(outMetrics)
|
||||
|
||||
val density = outMetrics.density
|
||||
|
||||
var adWidthPixels = adViewContainer.width.toFloat()
|
||||
if (adWidthPixels == 0f) {
|
||||
adWidthPixels = outMetrics.widthPixels.toFloat()
|
||||
}
|
||||
|
||||
val adWidth = (adWidthPixels / density).toInt()
|
||||
return AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(this, adWidth)
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package aculix.channelify.app.utils
|
||||
|
||||
import android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
import android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
import android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
import android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
import android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
|
||||
|
||||
/**
|
||||
* Class responsible for changing the view from full screen to non-full screen and vice versa.
|
||||
*/
|
||||
class FullScreenHelper(private val context: Activity, vararg views: View) {
|
||||
|
||||
var views: Array<View> = arrayOf(*views)
|
||||
|
||||
/**
|
||||
* call this method to enter full screen
|
||||
*/
|
||||
fun enterFullScreen() {
|
||||
val decorView = context.window.decorView
|
||||
|
||||
hideSystemUi(decorView)
|
||||
|
||||
for (view in views) {
|
||||
view.visibility = View.GONE
|
||||
view.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* call this method to exit full screen
|
||||
*/
|
||||
fun exitFullScreen() {
|
||||
val decorView = context.window.decorView
|
||||
|
||||
showSystemUi(decorView)
|
||||
|
||||
for (view in views) {
|
||||
view.visibility = View.VISIBLE
|
||||
view.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemUi(mDecorView: View) {
|
||||
mDecorView.systemUiVisibility = (SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
||||
}
|
||||
|
||||
private fun showSystemUi(mDecorView: View) {
|
||||
mDecorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.repository.AboutRepository
|
||||
import aculix.core.helper.ResultWrapper
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AboutViewModel(
|
||||
private val aboutRepository: AboutRepository,
|
||||
private val channelId: String,
|
||||
private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val _channelInfoLiveData = MutableLiveData<ResultWrapper>()
|
||||
val channelInfoLiveData: LiveData<ResultWrapper>
|
||||
get() = _channelInfoLiveData
|
||||
|
||||
fun getChannelInfo() {
|
||||
viewModelScope.launch {
|
||||
_channelInfoLiveData.value = ResultWrapper.Loading
|
||||
|
||||
val response = aboutRepository.getChannelInfo(channelId)
|
||||
if (response.isSuccessful) {
|
||||
_channelInfoLiveData.value = ResultWrapper.Success(response.body())
|
||||
} else {
|
||||
_channelInfoLiveData.value =
|
||||
ResultWrapper.Error(context.getString(R.string.error_channel_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.model.CommentReply
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.paging.datasourcefactory.CommentRepliesDataSourceFactory
|
||||
import aculix.channelify.app.repository.CommentRepliesRepository
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CommentRepliesViewModel(private val commentRepliesRepository: CommentRepliesRepository) :
|
||||
ViewModel() {
|
||||
|
||||
lateinit var commentRepliesDataSourceFactory: CommentRepliesDataSourceFactory
|
||||
var commentRepliesLiveData: LiveData<PagedList<CommentReply.Item>>? = null
|
||||
var networkStateLiveData: LiveData<NetworkState>? = null
|
||||
|
||||
fun getCommentReplies(commentId: String) {
|
||||
if (commentRepliesLiveData == null) {
|
||||
viewModelScope.launch {
|
||||
commentRepliesDataSourceFactory =
|
||||
CommentRepliesDataSourceFactory(
|
||||
commentRepliesRepository,
|
||||
viewModelScope,
|
||||
commentId
|
||||
)
|
||||
|
||||
commentRepliesLiveData =
|
||||
LivePagedListBuilder(commentRepliesDataSourceFactory, pagedListConfig()).build()
|
||||
networkStateLiveData =
|
||||
Transformations.switchMap(commentRepliesDataSourceFactory.commentRepliesDataSourceLiveData) { it.getNetworkState() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry possible last paged request (ie: network issue)
|
||||
*/
|
||||
fun refreshFailedRequest() =
|
||||
commentRepliesDataSourceFactory.getSource()?.retryFailedQuery()
|
||||
|
||||
private fun pagedListConfig() = PagedList.Config.Builder()
|
||||
.setInitialLoadSizeHint(Constants.INITIAL_PAGE_LOAD_SIZE)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPageSize(Constants.PAGE_SIZE)
|
||||
.build()
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.model.Comment
|
||||
import aculix.channelify.app.model.SearchedVideo
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.paging.datasourcefactory.CommentsDataSourceFactory
|
||||
import aculix.channelify.app.repository.CommentsRepository
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class CommentsViewModel(
|
||||
private val commentsRepository: CommentsRepository,
|
||||
private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
lateinit var commentsDataSourceFactory: CommentsDataSourceFactory
|
||||
var commentsLiveData: LiveData<PagedList<Comment.Item>>? = null
|
||||
var networkStateLiveData: LiveData<NetworkState>? = null
|
||||
private var _emptyStateLiveData = MutableLiveData<Boolean>()
|
||||
val emptyStateLiveData: LiveData<Boolean>
|
||||
get() = _emptyStateLiveData
|
||||
|
||||
|
||||
fun getVideoComments(videoId: String, sortOrder: String) {
|
||||
if (commentsLiveData == null) {
|
||||
viewModelScope.launch {
|
||||
commentsDataSourceFactory =
|
||||
CommentsDataSourceFactory(
|
||||
commentsRepository,
|
||||
viewModelScope,
|
||||
videoId,
|
||||
sortOrder
|
||||
)
|
||||
|
||||
commentsLiveData = LivePagedListBuilder(commentsDataSourceFactory, pagedListConfig())
|
||||
.setBoundaryCallback(object :
|
||||
PagedList.BoundaryCallback<Comment.Item>() {
|
||||
override fun onZeroItemsLoaded() {
|
||||
super.onZeroItemsLoaded()
|
||||
_emptyStateLiveData.value = true
|
||||
}
|
||||
|
||||
override fun onItemAtFrontLoaded(itemAtFront: Comment.Item) {
|
||||
super.onItemAtFrontLoaded(itemAtFront)
|
||||
_emptyStateLiveData.value = false
|
||||
}
|
||||
|
||||
override fun onItemAtEndLoaded(itemAtEnd: Comment.Item) {
|
||||
super.onItemAtEndLoaded(itemAtEnd)
|
||||
_emptyStateLiveData.value = false
|
||||
}
|
||||
})
|
||||
.build()
|
||||
networkStateLiveData = Transformations.switchMap(commentsDataSourceFactory.commentsDataSourceLiveData) { it.getNetworkState() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun sortComments(updatedSortOrder: String) {
|
||||
commentsDataSourceFactory.setSortOrder(updatedSortOrder)
|
||||
commentsDataSourceFactory.commentsDataSourceLiveData.value?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry possible last paged request (ie: network issue)
|
||||
*/
|
||||
fun refreshFailedRequest() =
|
||||
commentsDataSourceFactory.getSource()?.retryFailedQuery()
|
||||
|
||||
private fun pagedListConfig() = PagedList.Config.Builder()
|
||||
.setInitialLoadSizeHint(Constants.INITIAL_PAGE_LOAD_SIZE)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPageSize(Constants.PAGE_SIZE)
|
||||
.build()
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.model.FavoriteVideo
|
||||
import aculix.channelify.app.repository.FavoritesRepository
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FavoritesViewModel(private val favoritesRepository: FavoritesRepository) : ViewModel() {
|
||||
|
||||
var favoriteVideosLiveData: LiveData<List<FavoriteVideo>> = liveData(Dispatchers.IO) {
|
||||
emit(favoritesRepository.getFavoriteVideosFromDb())
|
||||
}
|
||||
|
||||
fun removeVideoFromFavorites(favoriteVideo: FavoriteVideo) {
|
||||
viewModelScope.launch {
|
||||
favoritesRepository.removeVideoFromFavorites(favoriteVideo)
|
||||
}
|
||||
}
|
||||
|
||||
fun addVideoToFavorites(favoriteVideo: FavoriteVideo) {
|
||||
viewModelScope.launch {
|
||||
favoritesRepository.addVideoToFavorites(favoriteVideo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.R
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.datasourcefactory.HomeDataSourceFactory
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.repository.HomeRepository
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import aculix.core.helper.ResultWrapper
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.*
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class HomeViewModel(
|
||||
private val homeRepository: HomeRepository,
|
||||
private val channelId: String,
|
||||
private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uploadsPlaylistIdLiveData = MutableLiveData<ResultWrapper>()
|
||||
val uploadsPlaylistIdLiveData: LiveData<ResultWrapper>
|
||||
get() = _uploadsPlaylistIdLiveData
|
||||
|
||||
lateinit var homeDataSourceFactory: HomeDataSourceFactory
|
||||
var latestVideoLiveData: LiveData<PagedList<PlaylistItemInfo.Item>>? = null
|
||||
var networkStateLiveData: LiveData<NetworkState>? = null
|
||||
|
||||
fun getLatestVideos() {
|
||||
if (latestVideoLiveData == null) {
|
||||
viewModelScope.launch {
|
||||
_uploadsPlaylistIdLiveData.value = ResultWrapper.Loading
|
||||
|
||||
val uploadsPlaylistIdRequest = async(Dispatchers.IO) { homeRepository.getUploadsPlaylistId(channelId) }
|
||||
val response = uploadsPlaylistIdRequest.await()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val playlistId = response.body()!!.items[0].contentDetails.relatedPlaylists.uploads
|
||||
homeDataSourceFactory =
|
||||
HomeDataSourceFactory(
|
||||
homeRepository,
|
||||
viewModelScope,
|
||||
playlistId
|
||||
)
|
||||
|
||||
latestVideoLiveData = LivePagedListBuilder(homeDataSourceFactory, pagedListConfig()).build()
|
||||
networkStateLiveData = Transformations.switchMap(homeDataSourceFactory.homeDataSourceLiveData) { it.getNetworkState() }
|
||||
_uploadsPlaylistIdLiveData.value = ResultWrapper.Success("")
|
||||
} else {
|
||||
Timber.e("Error: ${response.raw()}")
|
||||
_uploadsPlaylistIdLiveData.value = ResultWrapper.Error(context.getString(R.string.error_fetch_videos))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry possible last paged request (ie: network issue)
|
||||
*/
|
||||
fun refreshFailedRequest() =
|
||||
homeDataSourceFactory.getSource()?.retryFailedQuery()
|
||||
|
||||
private fun pagedListConfig() = PagedList.Config.Builder()
|
||||
.setInitialLoadSizeHint(Constants.INITIAL_PAGE_LOAD_SIZE)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPageSize(Constants.PAGE_SIZE)
|
||||
.build()
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package aculix.channelify.app.viewmodel
|
||||
|
||||
import aculix.channelify.app.model.PlaylistItemInfo
|
||||
import aculix.channelify.app.paging.NetworkState
|
||||
import aculix.channelify.app.paging.datasourcefactory.PlaylistVideosDataSourceFactory
|
||||
import aculix.channelify.app.repository.PlaylistVideosRepository
|
||||
import aculix.channelify.app.utils.Constants
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlaylistVideosViewModel(
|
||||
private val playlistVideosRepository: PlaylistVideosRepository
|
||||
) : ViewModel() {
|
||||
|
||||
lateinit var playlistVideosDataSourceFactory: PlaylistVideosDataSourceFactory
|
||||
var playlistVideosLiveData: LiveData<PagedList<PlaylistItemInfo.Item>>? = null
|
||||
var networkStateLiveData: LiveData<NetworkState>? = null
|
||||
|
||||
fun getPlaylistVideos(playlistId: String) {
|
||||
if (playlistVideosLiveData == null) {
|
||||
viewModelScope.launch {
|
||||
playlistVideosDataSourceFactory =
|
||||
PlaylistVideosDataSourceFactory(
|
||||
playlistVideosRepository,
|
||||
viewModelScope,
|
||||
playlistId
|
||||
)
|
||||
|
||||
playlistVideosLiveData =
|
||||
LivePagedListBuilder(playlistVideosDataSourceFactory, pagedListConfig()).build()
|
||||
networkStateLiveData =
|
||||
Transformations.switchMap(playlistVideosDataSourceFactory.playlistVideosDataSourceLiveData) { it.getNetworkState() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry possible last paged request (ie: network issue)
|
||||
*/
|
||||
fun refreshFailedRequest() =
|
||||
playlistVideosDataSourceFactory.getSource()?.retryFailedQuery()
|
||||
|
||||
private fun pagedListConfig() = PagedList.Config.Builder()
|
||||
.setInitialLoadSizeHint(Constants.INITIAL_PAGE_LOAD_SIZE)
|
||||
.setEnablePlaceholders(false)
|
||||
.setPageSize(Constants.PAGE_SIZE)
|
||||
.build()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user