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