Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a257651f8e | ||
|
|
0690c1c898 | ||
|
|
66ec39f566 | ||
|
|
f4abd1246d | ||
|
|
a17821181f | ||
|
|
4da68f86cb | ||
|
|
487bf3b7ee | ||
|
|
21f5256e0f | ||
|
|
41df9a59df | ||
|
|
3709bf5f5a | ||
|
|
3cdc1cdaba | ||
|
|
6031979f4c | ||
|
|
896f66254f | ||
|
|
3a18933b87 | ||
|
|
8c45084406 | ||
|
|
d4a297710b | ||
|
|
19f30c19b0 | ||
|
|
4e8e8bc232 | ||
|
|
ec5c751390 | ||
|
|
46b0b7aade | ||
|
|
3877739320 | ||
|
|
538eeb4f22 | ||
|
|
0d724c532a | ||
|
|
eba14e0bf4 | ||
|
|
a8c35b0ea0 | ||
|
|
17744729b2 | ||
|
|
fd5442097e | ||
|
|
f9eeae6bf0 | ||
|
|
c2e33b9b13 | ||
|
|
9c4b121574 | ||
|
|
ddb1be18bc | ||
|
|
4a4c1ed6ca | ||
|
|
c28759e165 | ||
|
|
45a5adbc0b | ||
|
|
118eacfa90 | ||
|
|
91ccf32f77 | ||
|
|
065e738773 | ||
|
|
65a7bbf501 | ||
|
|
d661c2faf3 | ||
|
|
8175dbd1c0 | ||
|
|
f068be6945 | ||
|
|
fe465db020 | ||
|
|
d6fe265b38 | ||
|
|
8eecfab5d7 | ||
|
|
0f9ffcacb9 | ||
|
|
aa7c45d462 | ||
|
|
156f5ecd3c | ||
|
|
e56fd0d024 | ||
|
|
06e245fe48 | ||
|
|
9f0fa644a7 | ||
|
|
5b213199dd | ||
|
|
b4f6273ede | ||
|
|
66c624e16c | ||
|
|
e47916f1c2 | ||
|
|
ab7875cbb3 | ||
|
|
5f15615a96 | ||
|
|
9cda86ab84 | ||
|
|
1873497fec |
@@ -2,17 +2,16 @@ root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
max_line_length = 140
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{*.kts,*.kt}]
|
||||
[{*.kts, *.kt}]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
# Disable wildcard imports in IntelliJ/Android Studio
|
||||
ij_kotlin_name_count_to_use_star_import = 1000
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 1000
|
||||
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||
ij_kotlin_packages_to_use_import_on_demand =
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
trim_trailing_whitespace = false
|
||||
8
.github/workflows/app-build.yaml
vendored
8
.github/workflows/app-build.yaml
vendored
@@ -15,20 +15,20 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
- name: Assemble debug APKs
|
||||
run: ./gradlew assembleDebug
|
||||
- name: Create publish bundle
|
||||
run: mkdir -p build/gh-app-publish/; find app/build/ -iname "*.apk" -exec mv "{}" build/gh-app-publish/ \;
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
||||
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
|
||||
with:
|
||||
name: build-artifacts
|
||||
retention-days: 14
|
||||
|
||||
8
.github/workflows/app-lint.yaml
vendored
8
.github/workflows/app-lint.yaml
vendored
@@ -17,18 +17,18 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
- name: Run detekt and lint tasks
|
||||
run: ./gradlew detekt lint
|
||||
- name: Upload SARIF files
|
||||
uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
|
||||
uses: github/codeql-action/upload-sarif@b374143c1149a9115d881581d29b8390bbcbb59c # v3.22.11
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
sarif_file: .
|
||||
|
||||
10
.github/workflows/app-publish.yaml
vendored
10
.github/workflows/app-publish.yaml
vendored
@@ -12,14 +12,14 @@ jobs:
|
||||
if: ${{ contains(github.repository_owner, 'jellyfin') }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
- name: Set JELLYFIN_VERSION
|
||||
run: echo "JELLYFIN_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | tr / -)" >> $GITHUB_ENV
|
||||
- name: Assemble release files
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
mv ${{ steps.signAab.outputs.signedReleaseFile }} build/jellyfin-publish/jellyfin-androidtv-v${{ env.JELLYFIN_VERSION }}-release.aab
|
||||
mv app/build/version.txt build/jellyfin-publish/
|
||||
- name: Upload release archive to GitHub release
|
||||
uses: alexellis/upload-assets@13926a61cdb2cb35f5fdef1c06b8b591523236d3 # 0.4.1
|
||||
uses: alexellis/upload-assets@259de5111cb56966d046ced998941e93f91d2c93 # tag=0.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
remote_user: ${{ secrets.REPO_USER }}
|
||||
remote_key: ${{ secrets.REPO_KEY }}
|
||||
- name: Update repo.jellyfin.org symlinks
|
||||
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
|
||||
uses: appleboy/ssh-action@55dabf81b49d4120609345970c91507e2d734799 # v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.REPO_HOST }}
|
||||
username: ${{ secrets.REPO_USER }}
|
||||
|
||||
6
.github/workflows/app-test.yaml
vendored
6
.github/workflows/app-test.yaml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1
|
||||
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/gradle-build-action@8cbcb9948b5892387aed077daf6f90e1f0ba5b27 # v2.11.0
|
||||
- name: Run test task
|
||||
run: ./gradlew test
|
||||
|
||||
4
.github/workflows/gradlew-validate.yaml
vendored
4
.github/workflows/gradlew-validate.yaml
vendored
@@ -17,6 +17,6 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0
|
||||
uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # v1.1.0
|
||||
|
||||
2
.github/workflows/repo-merge-conflict.yaml
vendored
2
.github/workflows/repo-merge-conflict.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ contains(github.repository_owner, 'jellyfin') }}
|
||||
steps:
|
||||
- uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
||||
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
|
||||
with:
|
||||
dirtyLabel: merge conflict
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,5 @@ local.properties
|
||||
|
||||
/.idea
|
||||
.gradle
|
||||
.kotlin/
|
||||
build/
|
||||
captures/
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
- [sparky3387](https://github.com/sparky3387)
|
||||
- [mohd-akram](https://github.com/mohd-akram)
|
||||
- [3l0w](https://github.com/3l0w)
|
||||
- [MajMongoose](https://github.com/majmongoose)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
39
app/baselineWorkaround.gradle
Normal file
39
app/baselineWorkaround.gradle
Normal file
@@ -0,0 +1,39 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath libs.android.gradle
|
||||
}
|
||||
}
|
||||
|
||||
import com.android.tools.profgen.ArtProfileKt
|
||||
import com.android.tools.profgen.ArtProfileSerializer
|
||||
import com.android.tools.profgen.DexFile
|
||||
|
||||
/**
|
||||
* This is a temporary workaround for https://issuetracker.google.com/issues/231837768
|
||||
* which will be fixed in AGP 8.1.0, at which point the workaround may be deleted.
|
||||
*/
|
||||
project.afterEvaluate {
|
||||
tasks.compileReleaseArtProfile.doLast {
|
||||
outputs.files.each { file ->
|
||||
if (file.toString().endsWith(".profm")) {
|
||||
println("Sorting ${file}")
|
||||
def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
|
||||
def profile = ArtProfileKt.ArtProfile(file)
|
||||
def keys = new ArrayList(profile.profileData.keySet())
|
||||
def sortedData = new LinkedHashMap()
|
||||
Collections.sort keys, new DexFile.Companion()
|
||||
keys.each { key -> sortedData[key] = profile.profileData[key] }
|
||||
new FileOutputStream(file).with {
|
||||
write(version.magicBytes$profgen)
|
||||
write(version.versionBytes$profgen)
|
||||
version.write$profgen(it, sortedData, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,19 @@ plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.aboutlibraries)
|
||||
}
|
||||
|
||||
// Apply workaround
|
||||
apply("baselineWorkaround.gradle")
|
||||
|
||||
android {
|
||||
namespace = "org.jellyfin.androidtv"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
|
||||
// Release version
|
||||
applicationId = namespace
|
||||
@@ -31,6 +33,10 @@ android {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
val release by getting {
|
||||
isMinifyEnabled = false
|
||||
@@ -80,7 +86,7 @@ aboutLibraries {
|
||||
}
|
||||
|
||||
val versionTxt by tasks.registering {
|
||||
val path = layout.buildDirectory.asFile.get().resolve("version.txt")
|
||||
val path = buildDir.resolve("version.txt")
|
||||
|
||||
doLast {
|
||||
val versionString = "v${android.defaultConfig.versionName}=${android.defaultConfig.versionCode}"
|
||||
@@ -92,9 +98,9 @@ val versionTxt by tasks.registering {
|
||||
dependencies {
|
||||
// Jellyfin
|
||||
implementation(projects.playback.core)
|
||||
implementation(projects.playback.exoplayer)
|
||||
implementation(projects.playback.jellyfin)
|
||||
implementation(projects.playback.media3.exoplayer)
|
||||
implementation(projects.playback.media3.session)
|
||||
implementation(projects.playback.ui)
|
||||
implementation(projects.preference)
|
||||
implementation(libs.jellyfin.apiclient)
|
||||
implementation(libs.jellyfin.sdk) {
|
||||
@@ -115,7 +121,6 @@ dependencies {
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.fragment.compose)
|
||||
implementation(libs.androidx.leanback.core)
|
||||
implementation(libs.androidx.leanback.preference)
|
||||
implementation(libs.androidx.preference)
|
||||
@@ -138,11 +143,13 @@ dependencies {
|
||||
implementation(libs.androidx.media3.exoplayer.hls)
|
||||
implementation(libs.androidx.media3.ui)
|
||||
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
|
||||
implementation(libs.libvlc)
|
||||
|
||||
// Markdown
|
||||
implementation(libs.bundles.markwon)
|
||||
|
||||
// Image utility
|
||||
implementation(libs.blurhash)
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
// Crash Reporting
|
||||
@@ -155,6 +162,10 @@ dependencies {
|
||||
implementation(libs.timber)
|
||||
implementation(libs.slf4j.timber)
|
||||
|
||||
// Debugging
|
||||
if (getProperty("leakcanary.enable")?.toBoolean() == true)
|
||||
debugImplementation(libs.leakcanary)
|
||||
|
||||
// Compatibility (desugaring)
|
||||
coreLibraryDesugaring(libs.android.desugar)
|
||||
|
||||
|
||||
@@ -141,5 +141,8 @@
|
||||
android:theme="@style/Theme.Jellyfin.Preferences" />
|
||||
|
||||
<activity android:name=".ui.playback.ExternalPlayerActivity" />
|
||||
|
||||
<!-- Playback Rewrite - Temporary -->
|
||||
<activity android:name=".ui.playback.rewrite.PlaybackForwardingActivity" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -2,11 +2,14 @@ package org.jellyfin.androidtv
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.await
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -15,6 +18,7 @@ import org.jellyfin.androidtv.data.eventhandling.SocketHandler
|
||||
import org.jellyfin.androidtv.data.repository.NotificationsRepository
|
||||
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
|
||||
import org.jellyfin.androidtv.telemetry.TelemetryService
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -35,6 +39,7 @@ class JellyfinApplication : Application() {
|
||||
*/
|
||||
suspend fun onSessionStart() = withContext(Dispatchers.IO) {
|
||||
val workManager by inject<WorkManager>()
|
||||
val autoBitrate by inject<AutoBitrate>()
|
||||
val socketListener by inject<SocketHandler>()
|
||||
|
||||
// Update background worker
|
||||
@@ -54,6 +59,16 @@ class JellyfinApplication : Application() {
|
||||
|
||||
// Update WebSockets
|
||||
launch { socketListener.updateSession() }
|
||||
|
||||
// Detect auto bitrate
|
||||
// running in a different scope to prevent slow startups
|
||||
ProcessLifecycleOwner.get().lifecycleScope.launch { autoBitrate.detect() }
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
|
||||
BlurHash.clearCache()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ data class AuthenticationStoreServer(
|
||||
@SerialName("login_disclaimer") val loginDisclaimer: String? = null,
|
||||
@SerialName("splashscreen_enabled") val splashscreenEnabled: Boolean = false,
|
||||
@SerialName("setup_completed") val setupCompleted: Boolean = true,
|
||||
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
|
||||
@SerialName("last_refreshed") val lastRefreshed: Long = Instant.now().toEpochMilli(),
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("last_refreshed") val lastRefreshed: Long = Date().time,
|
||||
val users: Map<UUID, AuthenticationStoreUser> = emptyMap(),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Locally stored user information. New properties require default values or deserialization will fail.
|
||||
@@ -10,7 +10,8 @@ import java.time.Instant
|
||||
@Serializable
|
||||
data class AuthenticationStoreUser(
|
||||
val name: String,
|
||||
@SerialName("last_used") val lastUsed: Long = Instant.now().toEpochMilli(),
|
||||
@SerialName("last_used") val lastUsed: Long = Date().time,
|
||||
@SerialName("require_password") val requirePassword: Boolean = true,
|
||||
@SerialName("image_tag") val imageTag: String? = null,
|
||||
@SerialName("access_token") val accessToken: String? = null,
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ package org.jellyfin.androidtv.auth.model
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
|
||||
sealed class LoginState
|
||||
data object AuthenticatingState : LoginState()
|
||||
data object RequireSignInState : LoginState()
|
||||
data object ServerUnavailableState : LoginState()
|
||||
object AuthenticatingState : LoginState()
|
||||
object RequireSignInState : LoginState()
|
||||
object ServerUnavailableState : LoginState()
|
||||
data class ServerVersionNotSupported(val server: Server) : LoginState()
|
||||
data class ApiClientErrorLoginState(val error: ApiClientException) : LoginState()
|
||||
data object AuthenticatedState : LoginState()
|
||||
object AuthenticatedState : LoginState()
|
||||
|
||||
@@ -5,12 +5,12 @@ sealed class QuickConnectState
|
||||
/**
|
||||
* State unknown untill first poll completed.
|
||||
*/
|
||||
data object UnknownQuickConnectState : QuickConnectState()
|
||||
object UnknownQuickConnectState : QuickConnectState()
|
||||
|
||||
/**
|
||||
* Server does not have QuickConnect enabled.
|
||||
*/
|
||||
data object UnavailableQuickConnectState : QuickConnectState()
|
||||
object UnavailableQuickConnectState : QuickConnectState()
|
||||
|
||||
/**
|
||||
* Connection is pending.
|
||||
@@ -20,4 +20,4 @@ data class PendingQuickConnectState(val code: String) : QuickConnectState()
|
||||
/**
|
||||
* User connected.
|
||||
*/
|
||||
data object ConnectedQuickConnectState : QuickConnectState()
|
||||
object ConnectedQuickConnectState : QuickConnectState()
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.jellyfin.androidtv.auth.model
|
||||
|
||||
import org.jellyfin.androidtv.auth.repository.ServerRepository
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -16,7 +16,7 @@ data class Server(
|
||||
val loginDisclaimer: String? = null,
|
||||
val splashscreenEnabled: Boolean = false,
|
||||
val setupCompleted: Boolean = true,
|
||||
var dateLastAccessed: Instant = Instant.MIN,
|
||||
var dateLastAccessed: Date = Date(0),
|
||||
) {
|
||||
private val serverVersion = version?.let(ServerVersion::fromString)
|
||||
val versionSupported = serverVersion != null && serverVersion >= ServerRepository.minimumServerVersion
|
||||
|
||||
@@ -10,6 +10,7 @@ sealed class User {
|
||||
abstract val serverId: UUID
|
||||
abstract val name: String
|
||||
abstract val accessToken: String?
|
||||
abstract val requirePassword: Boolean
|
||||
abstract val imageTag: String?
|
||||
|
||||
abstract fun withToken(accessToken: String): User
|
||||
@@ -33,6 +34,7 @@ data class PrivateUser(
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
val lastUsed: Long,
|
||||
) : User() {
|
||||
@@ -47,6 +49,7 @@ data class PublicUser(
|
||||
override val serverId: UUID,
|
||||
override val name: String,
|
||||
override val accessToken: String?,
|
||||
override val requirePassword: Boolean,
|
||||
override val imageTag: String?,
|
||||
) : User() {
|
||||
override fun withToken(accessToken: String) = copy(accessToken = accessToken)
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.jellyfin.androidtv.auth.model.ServerVersionNotSupported
|
||||
import org.jellyfin.androidtv.auth.model.User
|
||||
import org.jellyfin.androidtv.auth.store.AuthenticationPreferences
|
||||
import org.jellyfin.androidtv.auth.store.AuthenticationStore
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.sdk.forUser
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
@@ -33,9 +33,10 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Repository to manage authentication of the user in the app.
|
||||
@@ -66,7 +67,7 @@ class AuthenticationRepositoryImpl(
|
||||
}
|
||||
|
||||
private fun authenticateAutomatic(server: Server, user: User): Flow<LoginState> {
|
||||
Timber.i("Authenticating user ${user.id}")
|
||||
Timber.i("Authenticating user %s", user)
|
||||
|
||||
// Automatic logic is disabled when the always authenticate preference is enabled
|
||||
if (authenticationPreferences[AuthenticationPreferences.alwaysAuthenticate]) return flowOf(RequireSignInState)
|
||||
@@ -74,6 +75,8 @@ class AuthenticationRepositoryImpl(
|
||||
val authStoreUser = authenticationStore.getUser(server.id, user.id)
|
||||
// Try login with access token
|
||||
return if (authStoreUser?.accessToken != null) authenticateToken(server, user.withToken(authStoreUser.accessToken))
|
||||
// Try login without password
|
||||
else if (!user.requirePassword) authenticateCredential(server, user.name, "")
|
||||
// Require login
|
||||
else flowOf(RequireSignInState)
|
||||
}
|
||||
@@ -122,8 +125,9 @@ class AuthenticationRepositoryImpl(
|
||||
serverId = server.id,
|
||||
name = userInfo.name!!,
|
||||
accessToken = result.accessToken,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
lastUsed = Instant.now().toEpochMilli(),
|
||||
lastUsed = Date().time,
|
||||
)
|
||||
|
||||
authenticateFinish(server, userInfo, accessToken)
|
||||
@@ -162,11 +166,13 @@ class AuthenticationRepositoryImpl(
|
||||
|
||||
val updatedUser = currentUser?.copy(
|
||||
name = userInfo.name!!,
|
||||
lastUsed = Instant.now().toEpochMilli(),
|
||||
lastUsed = Date().time,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
accessToken = accessToken,
|
||||
) ?: AuthenticationStoreUser(
|
||||
name = userInfo.name!!,
|
||||
requirePassword = userInfo.hasPassword,
|
||||
imageTag = userInfo.primaryImageTag,
|
||||
accessToken = accessToken,
|
||||
)
|
||||
@@ -179,11 +185,11 @@ class AuthenticationRepositoryImpl(
|
||||
if (authenticated) {
|
||||
// Update last use in store
|
||||
authenticationStore.getServer(server.id)?.let { storedServer ->
|
||||
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Instant.now().toEpochMilli()))
|
||||
authenticationStore.putServer(server.id, storedServer.copy(lastUsed = Date().time))
|
||||
}
|
||||
|
||||
authenticationStore.getUser(server.id, user.id)?.let { storedUser ->
|
||||
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Instant.now().toEpochMilli()))
|
||||
authenticationStore.putUser(server.id, user.id, storedUser.copy(lastUsed = Date().time))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +209,8 @@ class AuthenticationRepositoryImpl(
|
||||
jellyfin.createApi(server.address).imageApi.getUserImageUrl(
|
||||
userId = user.id,
|
||||
tag = tag,
|
||||
maxHeight = ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT
|
||||
imageType = ImageType.PRIMARY,
|
||||
maxHeight = ImageUtils.MAX_PRIMARY_IMAGE_HEIGHT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.jellyfin.sdk.model.api.BrandingOptions
|
||||
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ class ServerRepositoryImpl(
|
||||
loginDisclaimer = branding.loginDisclaimer,
|
||||
splashscreenEnabled = branding.splashscreenEnabled,
|
||||
setupCompleted = systemInfo.startupWizardCompleted ?: true,
|
||||
lastUsed = Instant.now().toEpochMilli()
|
||||
lastUsed = Date().time
|
||||
) ?: AuthenticationStoreServer(
|
||||
name = systemInfo.serverName ?: "Jellyfin Server",
|
||||
address = chosenRecommendation.address,
|
||||
@@ -188,7 +188,7 @@ class ServerRepositoryImpl(
|
||||
}
|
||||
|
||||
private suspend fun updateServerInternal(id: UUID, server: AuthenticationStoreServer): AuthenticationStoreServer? {
|
||||
val now = Instant.now().toEpochMilli()
|
||||
val now = Date().time
|
||||
|
||||
// Only update every 10 minutes
|
||||
if (now - server.lastRefreshed < 600000 && server.version != null) return null
|
||||
@@ -226,7 +226,7 @@ class ServerRepositoryImpl(
|
||||
loginDisclaimer = loginDisclaimer,
|
||||
splashscreenEnabled = splashscreenEnabled,
|
||||
setupCompleted = setupCompleted,
|
||||
dateLastAccessed = Instant.ofEpochMilli(lastUsed),
|
||||
dateLastAccessed = Date(lastUsed),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,7 @@ class ServerUserRepositoryImpl(
|
||||
serverId = server.id,
|
||||
name = userInfo.name,
|
||||
accessToken = authInfo?.accessToken,
|
||||
requirePassword = userInfo.requirePassword,
|
||||
imageTag = userInfo.imageTag,
|
||||
lastUsed = userInfo.lastUsed,
|
||||
)
|
||||
|
||||
@@ -141,6 +141,11 @@ class SessionRepositoryImpl(
|
||||
val applied = userApiClient.applySession(session, deviceInfo)
|
||||
|
||||
if (applied && session != null) {
|
||||
// Update crash reporting URL
|
||||
val crashReportUrl = userApiClient.clientLogApi.logFileUrl(includeCredentials = false)
|
||||
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
|
||||
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
|
||||
|
||||
try {
|
||||
val user by userApiClient.userApi.getCurrentUser()
|
||||
userRepository.updateCurrentUser(user)
|
||||
@@ -149,11 +154,6 @@ class SessionRepositoryImpl(
|
||||
destroyCurrentSession()
|
||||
return false
|
||||
}
|
||||
|
||||
// Update crash reporting URL
|
||||
val crashReportUrl = userApiClient.clientLogApi.logFileUrl()
|
||||
telemetryPreferences[TelemetryPreferences.crashReportUrl] = crashReportUrl
|
||||
telemetryPreferences[TelemetryPreferences.crashReportToken] = session.accessToken
|
||||
} else {
|
||||
userRepository.updateCurrentUser(null)
|
||||
}
|
||||
@@ -184,21 +184,18 @@ class SessionRepositoryImpl(
|
||||
}
|
||||
|
||||
private fun ApiClient.applySession(session: Session?, newDeviceInfo: DeviceInfo = defaultDeviceInfo): Boolean {
|
||||
deviceInfo = newDeviceInfo
|
||||
|
||||
if (session == null) {
|
||||
update(
|
||||
baseUrl = null,
|
||||
accessToken = null,
|
||||
deviceInfo = newDeviceInfo,
|
||||
)
|
||||
baseUrl = null
|
||||
accessToken = null
|
||||
userId = null
|
||||
} else {
|
||||
val server = authenticationStore.getServer(session.serverId)
|
||||
?: return false
|
||||
|
||||
update(
|
||||
baseUrl = server.address,
|
||||
accessToken = session.accessToken,
|
||||
deviceInfo = newDeviceInfo,
|
||||
)
|
||||
baseUrl = server.address
|
||||
accessToken = session.accessToken
|
||||
userId = session.userId
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -17,6 +17,6 @@ class UserRepositoryImpl : UserRepository {
|
||||
override val currentUser = MutableStateFlow<UserDto?>(null)
|
||||
|
||||
override fun updateCurrentUser(user: UserDto?) {
|
||||
currentUser.value = user
|
||||
if (currentUser.value !== user) currentUser.value = user
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ enum class ChangeTriggerType {
|
||||
LibraryUpdated,
|
||||
MoviePlayback,
|
||||
TvPlayback,
|
||||
GuideNeedsLoad,
|
||||
MusicPlayback,
|
||||
Always,
|
||||
VideoQueueChange,
|
||||
FavoriteUpdate,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ object Codec {
|
||||
const val ASF = "asf"
|
||||
const val AVI = "avi"
|
||||
const val DVR_MS = "dvr-ms"
|
||||
const val HLS = "hls"
|
||||
const val M2V = "m2v"
|
||||
const val M4V = "m4v"
|
||||
const val MKV = "mkv"
|
||||
@@ -48,10 +47,8 @@ object Codec {
|
||||
const val PCM_ALAW = "pcm_alaw"
|
||||
const val PCM_MULAW = "pcm_mulaw"
|
||||
const val PCM_S16LE = "pcm_s16le"
|
||||
const val PCM_S20LE = "pcm_s20le"
|
||||
const val PCM_S24LE = "pcm_s24le"
|
||||
const val TRUEHD = "truehd"
|
||||
const val VORBIS = "vorbis"
|
||||
const val WAV = "wav"
|
||||
const val WEBMA = "webma"
|
||||
const val WMA = "wma"
|
||||
|
||||
@@ -2,6 +2,5 @@ package org.jellyfin.androidtv.constant
|
||||
|
||||
object Extras {
|
||||
const val Folder = "folder"
|
||||
const val IsLiveTvSeriesRecordings = "is_livetv_series_recordings"
|
||||
const val IncludeType = "type_include"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import org.jellyfin.androidtv.R
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private val qualityOptions = setOf(
|
||||
200.0, 180.0, 140.0, 120.0, 110.0, 100.0, // 100 >=
|
||||
0.0, // auto
|
||||
120.0, 110.0, 100.0, // 100 >=
|
||||
90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 15.0, 10.0, // 10 >=
|
||||
5.0, 3.0, 2.0, 1.0, // 1 >=
|
||||
0.72, 0.42 // 0 >=
|
||||
@@ -16,6 +17,7 @@ fun getQualityProfiles(
|
||||
context: Context
|
||||
): Map<String, String> = qualityOptions.associate {
|
||||
val value = when {
|
||||
it == 0.0 -> context.getString(R.string.bitrate_auto)
|
||||
it >= 1.0 -> context.getString(R.string.bitrate_mbit, it)
|
||||
else -> context.getString(R.string.bitrate_kbit, it * 1000.0)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ enum class QueryType {
|
||||
LiveTvProgram,
|
||||
LiveTvRecording,
|
||||
StaticItems,
|
||||
Persons,
|
||||
StaticAudioQueueItems,
|
||||
Artists,
|
||||
AlbumArtists,
|
||||
|
||||
@@ -3,12 +3,11 @@ package org.jellyfin.androidtv.data.compat
|
||||
import org.jellyfin.apiclient.model.dlna.DeviceProfile
|
||||
import org.jellyfin.apiclient.model.dlna.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import java.util.UUID
|
||||
|
||||
open class AudioOptions {
|
||||
var enableDirectPlay = true
|
||||
var enableDirectStream = true
|
||||
var itemId: UUID? = null
|
||||
var itemId: String? = null
|
||||
var mediaSources: List<MediaSourceInfo>? = null
|
||||
var profile: DeviceProfile? = null
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.jellyfin.androidtv.data.compat;
|
||||
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
|
||||
import org.jellyfin.apiclient.model.dlna.DlnaProfileType;
|
||||
import org.jellyfin.apiclient.model.dlna.EncodingContext;
|
||||
import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod;
|
||||
import org.jellyfin.apiclient.model.dlna.SubtitleProfile;
|
||||
import org.jellyfin.apiclient.model.dlna.TranscodeSeekInfo;
|
||||
import org.jellyfin.apiclient.model.dto.NameValuePair;
|
||||
import org.jellyfin.apiclient.model.session.PlayMethod;
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol;
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo;
|
||||
@@ -13,16 +15,23 @@ import org.jellyfin.sdk.model.api.MediaStream;
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class StreamInfo {
|
||||
private UUID ItemId;
|
||||
private static final String START_TIME_TICKS = "StartTimeTicks";
|
||||
private static final String SUBTITLE_STREAM_INDEX = "SubtitleStreamIndex";
|
||||
private static final String STATIC = "Static";
|
||||
|
||||
public final UUID getItemId() {
|
||||
public StreamInfo() {
|
||||
setAudioCodecs(new String[]{});
|
||||
}
|
||||
|
||||
private String ItemId;
|
||||
|
||||
public final String getItemId() {
|
||||
return ItemId;
|
||||
}
|
||||
|
||||
public final void setItemId(UUID value) {
|
||||
public final void setItemId(String value) {
|
||||
ItemId = value;
|
||||
}
|
||||
|
||||
@@ -56,6 +65,16 @@ public class StreamInfo {
|
||||
Context = value;
|
||||
}
|
||||
|
||||
private DlnaProfileType MediaType = DlnaProfileType.values()[0];
|
||||
|
||||
public final DlnaProfileType getMediaType() {
|
||||
return MediaType;
|
||||
}
|
||||
|
||||
public final void setMediaType(DlnaProfileType value) {
|
||||
MediaType = value;
|
||||
}
|
||||
|
||||
private String Container;
|
||||
|
||||
public final String getContainer() {
|
||||
@@ -76,6 +95,16 @@ public class StreamInfo {
|
||||
StartPositionTicks = value;
|
||||
}
|
||||
|
||||
private String[] AudioCodecs;
|
||||
|
||||
public final String[] getAudioCodecs() {
|
||||
return AudioCodecs;
|
||||
}
|
||||
|
||||
public final void setAudioCodecs(String[] value) {
|
||||
AudioCodecs = value;
|
||||
}
|
||||
|
||||
private DeviceProfile DeviceProfile;
|
||||
|
||||
public final DeviceProfile getDeviceProfile() {
|
||||
@@ -86,6 +115,16 @@ public class StreamInfo {
|
||||
DeviceProfile = value;
|
||||
}
|
||||
|
||||
private String DeviceId;
|
||||
|
||||
public final String getDeviceId() {
|
||||
return DeviceId;
|
||||
}
|
||||
|
||||
public final void setDeviceId(String value) {
|
||||
DeviceId = value;
|
||||
}
|
||||
|
||||
private Long RunTimeTicks = null;
|
||||
|
||||
public final Long getRunTimeTicks() {
|
||||
@@ -132,6 +171,122 @@ public class StreamInfo {
|
||||
return getMediaSource() == null ? null : getMediaSource().getId();
|
||||
}
|
||||
|
||||
public final boolean getIsDirectStream() {
|
||||
return getPlayMethod() == PlayMethod.DirectStream || getPlayMethod() == PlayMethod.DirectPlay;
|
||||
}
|
||||
|
||||
public final String toUrl(String baseUrl, String accessToken) {
|
||||
if (!Utils.isEmpty(getMediaUrl())) {
|
||||
return getMediaUrl();
|
||||
}
|
||||
|
||||
if (getPlayMethod() == PlayMethod.DirectPlay) {
|
||||
return getMediaSource().getPath();
|
||||
}
|
||||
|
||||
if (Utils.isEmpty(baseUrl)) {
|
||||
throw new IllegalArgumentException(baseUrl);
|
||||
}
|
||||
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
for (NameValuePair pair : buildParams(this, accessToken, false)) {
|
||||
if (Utils.isEmpty(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to keep the url clean by omitting defaults
|
||||
if (START_TIME_TICKS.equalsIgnoreCase(pair.getName()) && "0".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
if (SUBTITLE_STREAM_INDEX.equalsIgnoreCase(pair.getName()) && "-1".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
if (STATIC.equalsIgnoreCase(pair.getName()) && "false".equalsIgnoreCase(pair.getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
list.add(String.format("%1$s=%2$s", pair.getName(), pair.getValue()));
|
||||
}
|
||||
|
||||
String queryString = Utils.join("&", list);
|
||||
|
||||
return getUrl(baseUrl, queryString);
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String queryString) {
|
||||
if (Utils.isEmpty(baseUrl)) {
|
||||
throw new IllegalArgumentException(baseUrl);
|
||||
}
|
||||
|
||||
String extension = Utils.isEmpty(getContainer()) ? "" : "." + getContainer();
|
||||
|
||||
// remove trailing slashes
|
||||
baseUrl = baseUrl.replaceAll("[/]+$", "");
|
||||
|
||||
if (getMediaType() == DlnaProfileType.Audio) {
|
||||
return String.format("%1$s/audio/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
|
||||
}
|
||||
|
||||
return String.format("%1$s/videos/%2$s/stream%3$s?%4$s", baseUrl, getItemId(), extension, queryString);
|
||||
}
|
||||
|
||||
private static ArrayList<NameValuePair> buildParams(StreamInfo item, String accessToken, boolean isDlna) {
|
||||
ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
|
||||
|
||||
String audioCodecs = item.getAudioCodecs().length == 0 ? "" : Utils.join(",", item.getAudioCodecs());
|
||||
|
||||
list.add(new NameValuePair("DeviceProfileId", ""));
|
||||
String tempVar2 = item.getDeviceId();
|
||||
list.add(new NameValuePair("DeviceId", (tempVar2 != null) ? tempVar2 : ""));
|
||||
String tempVar3 = item.getMediaSourceId();
|
||||
list.add(new NameValuePair("MediaSourceId", (tempVar3 != null) ? tempVar3 : ""));
|
||||
list.add(new NameValuePair(STATIC, Boolean.valueOf(item.getIsDirectStream()).toString().toLowerCase()));
|
||||
list.add(new NameValuePair("VideoCodec", ""));
|
||||
list.add(new NameValuePair("AudioCodec", audioCodecs));
|
||||
list.add(new NameValuePair("AudioStreamIndex", ""));
|
||||
list.add(new NameValuePair("SubtitleStreamIndex", ""));
|
||||
list.add(new NameValuePair("VideoBitrate", ""));
|
||||
list.add(new NameValuePair("AudioBitrate", ""));
|
||||
list.add(new NameValuePair("MaxAudioChannels", ""));
|
||||
list.add(new NameValuePair("MaxFramerate", ""));
|
||||
list.add(new NameValuePair("MaxWidth", ""));
|
||||
list.add(new NameValuePair("MaxHeight", ""));
|
||||
|
||||
list.add(new NameValuePair(START_TIME_TICKS, String.valueOf(item.getStartPositionTicks())));
|
||||
|
||||
list.add(new NameValuePair("Level", ""));
|
||||
|
||||
list.add(new NameValuePair("MaxRefFrames", ""));
|
||||
list.add(new NameValuePair("MaxVideoBitDepth", ""));
|
||||
list.add(new NameValuePair("Profile", ""));
|
||||
|
||||
// no longer used
|
||||
list.add(new NameValuePair("Cabac", ""));
|
||||
|
||||
String tempVar6 = item.getPlaySessionId();
|
||||
list.add(new NameValuePair("PlaySessionId", (tempVar6 != null) ? tempVar6 : ""));
|
||||
list.add(new NameValuePair("api_key", (accessToken != null) ? accessToken : ""));
|
||||
|
||||
String liveStreamId = item.getMediaSource() == null ? null : item.getMediaSource().getLiveStreamId();
|
||||
list.add(new NameValuePair("LiveStreamId", (liveStreamId != null) ? liveStreamId : ""));
|
||||
|
||||
if (isDlna) {
|
||||
list.add(new NameValuePair("ItemId", item.getItemId()));
|
||||
}
|
||||
|
||||
list.add(new NameValuePair("CopyTimestamps", "false"));
|
||||
list.add(new NameValuePair("ForceLiveStream", "false"));
|
||||
list.add(new NameValuePair("SubtitleMethod", ""));
|
||||
|
||||
list.add(new NameValuePair("TranscodingMaxAudioChannels", ""));
|
||||
list.add(new NameValuePair("EnableSubtitlesInManifest", "false"));
|
||||
|
||||
String tempVar7 = item.getMediaSource().getETag();
|
||||
list.add(new NameValuePair("Tag", (tempVar7 != null) ? tempVar7 : ""));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public final ArrayList<SubtitleStreamInfo> getSubtitleProfiles(boolean includeSelectedTrackOnly, String baseUrl, String accessToken) {
|
||||
return getSubtitleProfiles(includeSelectedTrackOnly, false, baseUrl, accessToken);
|
||||
}
|
||||
|
||||
@@ -6,61 +6,57 @@ import android.os.Build
|
||||
import android.widget.Toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
|
||||
import org.jellyfin.androidtv.util.PlaybackHelper
|
||||
import org.jellyfin.androidtv.ui.playback.rewrite.RewriteMediaManager
|
||||
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.sessionApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.sockets.subscribe
|
||||
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommand
|
||||
import org.jellyfin.sdk.api.sockets.subscribeGeneralCommands
|
||||
import org.jellyfin.sdk.api.sockets.SocketInstance
|
||||
import org.jellyfin.sdk.api.sockets.SocketInstanceState
|
||||
import org.jellyfin.sdk.api.sockets.addGeneralCommandsListener
|
||||
import org.jellyfin.sdk.api.sockets.addListener
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.GeneralCommandType
|
||||
import org.jellyfin.sdk.model.api.LibraryChangedMessage
|
||||
import org.jellyfin.sdk.model.api.LibraryUpdateInfo
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlayMessage
|
||||
import org.jellyfin.sdk.model.api.PlaystateCommand
|
||||
import org.jellyfin.sdk.model.api.PlaystateMessage
|
||||
import org.jellyfin.sdk.model.extensions.get
|
||||
import org.jellyfin.sdk.model.constant.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.getValue
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import org.jellyfin.sdk.model.socket.LibraryChangedMessage
|
||||
import org.jellyfin.sdk.model.socket.PlayMessage
|
||||
import org.jellyfin.sdk.model.socket.PlayStateMessage
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class SocketHandler(
|
||||
private val context: Context,
|
||||
private val api: ApiClient,
|
||||
private val socketInstance: SocketInstance,
|
||||
private val dataRefreshService: DataRefreshService,
|
||||
private val mediaManager: MediaManager,
|
||||
private val playbackControllerContainer: PlaybackControllerContainer,
|
||||
private val navigationRepository: NavigationRepository,
|
||||
private val audioManager: AudioManager,
|
||||
private val itemLauncher: ItemLauncher,
|
||||
private val playbackHelper: PlaybackHelper,
|
||||
) {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
val state = socketInstance.state
|
||||
|
||||
suspend fun updateSession() {
|
||||
try {
|
||||
api.sessionApi.postCapabilities(
|
||||
playableMediaTypes = listOf(MediaType.VIDEO, MediaType.AUDIO),
|
||||
playableMediaTypes = listOf(MediaType.Video, MediaType.Audio),
|
||||
supportsMediaControl = true,
|
||||
supportedCommands = buildList {
|
||||
add(GeneralCommandType.DISPLAY_CONTENT)
|
||||
add(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
|
||||
add(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
|
||||
|
||||
add(GeneralCommandType.DISPLAY_MESSAGE)
|
||||
add(GeneralCommandType.SEND_STRING)
|
||||
@@ -81,70 +77,38 @@ class SocketHandler(
|
||||
Timber.e(err, "Unable to update capabilities")
|
||||
return
|
||||
}
|
||||
|
||||
val isOffline = state.value == SocketInstanceState.DISCONNECTED || state.value == SocketInstanceState.ERROR
|
||||
if (isOffline) socketInstance.reconnect()
|
||||
else socketInstance.updateCredentials()
|
||||
}
|
||||
|
||||
init {
|
||||
api.webSocket.apply {
|
||||
socketInstance.apply {
|
||||
// Library
|
||||
subscribe<LibraryChangedMessage>()
|
||||
.onEach { message -> message.data?.let(::onLibraryChanged) }
|
||||
.launchIn(coroutineScope)
|
||||
addListener<LibraryChangedMessage> { message -> onLibraryChanged(message.info) }
|
||||
|
||||
// Media playback
|
||||
subscribe<PlayMessage>()
|
||||
.onEach { message -> onPlayMessage(message) }
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribe<PlaystateMessage>()
|
||||
.onEach { message -> onPlayStateMessage(message) }
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribeGeneralCommand(GeneralCommandType.SET_SUBTITLE_STREAM_INDEX)
|
||||
.onEach { message ->
|
||||
val index = message["index"]?.toIntOrNull() ?: return@onEach
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playbackControllerContainer.playbackController?.switchSubtitleStream(index)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
|
||||
subscribeGeneralCommand(GeneralCommandType.SET_AUDIO_STREAM_INDEX)
|
||||
.onEach { message ->
|
||||
val index = message["index"]?.toIntOrNull() ?: return@onEach
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playbackControllerContainer.playbackController?.switchAudioStream(index)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
addListener<PlayMessage> { message -> onPlayMessage(message) }
|
||||
addListener<PlayStateMessage> { message -> onPlayStateMessage(message) }
|
||||
|
||||
// General commands
|
||||
subscribeGeneralCommand(GeneralCommandType.DISPLAY_CONTENT)
|
||||
.onEach { message ->
|
||||
val itemId by message
|
||||
val itemType by message
|
||||
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_CONTENT)) { message ->
|
||||
val itemId by message
|
||||
val itemType by message
|
||||
|
||||
val itemUuid = itemId?.toUUIDOrNull()
|
||||
val itemKind = itemType?.let { type ->
|
||||
BaseItemKind.entries.find { value ->
|
||||
value.serialName.equals(type, true)
|
||||
}
|
||||
}
|
||||
val itemUuid = itemId?.toUUIDOrNull()
|
||||
val itemKind = itemType?.let { type -> BaseItemKind.values().find { value -> value.serialName.equals(type, true) } }
|
||||
|
||||
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
if (itemUuid != null && itemKind != null) onDisplayContent(itemUuid, itemKind)
|
||||
}
|
||||
addGeneralCommandsListener(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING)) { message ->
|
||||
val header by message
|
||||
val text by message
|
||||
val string by message
|
||||
|
||||
subscribeGeneralCommands(setOf(GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SEND_STRING))
|
||||
.onEach { message ->
|
||||
val header by message
|
||||
val text by message
|
||||
val string by message
|
||||
|
||||
onDisplayMessage(header, text ?: string)
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
onDisplayMessage(header, text ?: string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,69 +121,80 @@ class SocketHandler(
|
||||
})
|
||||
|
||||
if (info.itemsAdded.any() || info.itemsRemoved.any())
|
||||
dataRefreshService.lastLibraryChange = Instant.now()
|
||||
dataRefreshService.lastLibraryChange = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun onPlayMessage(message: PlayMessage) {
|
||||
val itemId = message.data?.itemIds?.firstOrNull() ?: return
|
||||
val itemId = message.request.itemIds?.firstOrNull() ?: return
|
||||
|
||||
runCatching {
|
||||
playbackHelper.retrieveAndPlay(
|
||||
itemId,
|
||||
PlaybackHelper.retrieveAndPlay(
|
||||
itemId.toString(),
|
||||
false,
|
||||
message.data?.startPositionTicks,
|
||||
message.request.startPositionTicks,
|
||||
context
|
||||
)
|
||||
}.onFailure { Timber.w(it, "Failed to start playback") }
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private suspend fun onPlayStateMessage(message: PlaystateMessage) = withContext(Dispatchers.Main) {
|
||||
Timber.i("Received PlayStateMessage with command ${message.data?.command}")
|
||||
private fun onPlayStateMessage(message: PlayStateMessage) = coroutineScope.launch(Dispatchers.Main) {
|
||||
Timber.i("Received PlayStateMessage with command ${message.request.command}")
|
||||
|
||||
// Audio playback uses (Rewrite)MediaManager, (legacy) video playback uses playbackController
|
||||
when {
|
||||
mediaManager.hasAudioQueueItems() -> {
|
||||
// Ignore RewriteMediaManager
|
||||
mediaManager is RewriteMediaManager && mediaManager.hasAudioQueueItems() -> {
|
||||
Timber.i("Ignoring PlayStateMessage: should be handled by PlaySessionSocketService")
|
||||
return@withContext
|
||||
return@launch
|
||||
}
|
||||
|
||||
// LegacyMediaManager
|
||||
mediaManager.hasAudioQueueItems() -> when (message.request.command) {
|
||||
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
|
||||
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
|
||||
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
|
||||
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
|
||||
// Not implemented
|
||||
PlaystateCommand.SEEK,
|
||||
PlaystateCommand.REWIND,
|
||||
PlaystateCommand.FAST_FORWARD -> Unit
|
||||
}
|
||||
|
||||
// PlaybackController
|
||||
else -> {
|
||||
val playbackController = playbackControllerContainer.playbackController
|
||||
when (message.data?.command) {
|
||||
when (message.request.command) {
|
||||
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
|
||||
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
|
||||
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
|
||||
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
|
||||
PlaystateCommand.SEEK -> playbackController?.seek(
|
||||
(message.data?.seekPositionTicks ?: 0) / TICKS_TO_MS
|
||||
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
|
||||
)
|
||||
|
||||
PlaystateCommand.REWIND -> playbackController?.rewind()
|
||||
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) = withContext(Dispatchers.Main) {
|
||||
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) = coroutineScope.launch(Dispatchers.Main) {
|
||||
val playbackController = playbackControllerContainer.playbackController
|
||||
|
||||
if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
|
||||
Timber.i("Not launching $itemId: playback in progress")
|
||||
return@withContext
|
||||
return@launch
|
||||
}
|
||||
|
||||
Timber.i("Launching $itemId")
|
||||
|
||||
when (itemKind) {
|
||||
BaseItemKind.USER_VIEW,
|
||||
BaseItemKind.COLLECTION_FOLDER -> {
|
||||
BaseItemKind.COLLECTION_FOLDER -> coroutineScope.launch {
|
||||
val item by api.userLibraryApi.getItem(itemId = itemId)
|
||||
itemLauncher.launchUserView(item)
|
||||
ItemLauncher.launchUserView(item)
|
||||
}
|
||||
|
||||
else -> navigationRepository.navigate(Destinations.itemDetails(itemId))
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class DataRefreshService {
|
||||
var lastDeletedItemId: UUID? = null
|
||||
var lastPlayback: Instant? = null
|
||||
var lastMoviePlayback: Instant? = null
|
||||
var lastTvPlayback: Instant? = null
|
||||
var lastLibraryChange: Instant? = null
|
||||
var lastFavoriteUpdate: Instant? = null
|
||||
var lastPlayback: Long = 0
|
||||
var lastMoviePlayback: Long = 0
|
||||
var lastTvPlayback: Long = 0
|
||||
var lastMusicPlayback: Long = 0
|
||||
var lastLibraryChange: Long = 0
|
||||
var lastVideoQueueChange: Long = 0
|
||||
var lastFavoriteUpdate: Long = 0
|
||||
var lastPlayedItem: BaseItemDto? = null
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package org.jellyfin.androidtv.data.model
|
||||
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter
|
||||
|
||||
class FilterOptions {
|
||||
var isFavoriteOnly = false
|
||||
var isUnwatchedOnly = false
|
||||
|
||||
val filters: Set<ItemFilter>
|
||||
get() = buildSet {
|
||||
if (isFavoriteOnly) add(ItemFilter.IS_FAVORITE)
|
||||
if (isUnwatchedOnly) add(ItemFilter.IS_UNPLAYED)
|
||||
val filters: Array<ItemFilter>?
|
||||
get() {
|
||||
if (!isUnwatchedOnly && !isFavoriteOnly) return null
|
||||
|
||||
return buildList {
|
||||
if (isFavoriteOnly) add(ItemFilter.IsFavorite)
|
||||
if (isUnwatchedOnly) add(ItemFilter.IsUnplayed)
|
||||
}.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.jellyfin.androidtv.data.querying;
|
||||
|
||||
data class AdditionalPartsQuery(val itemId: String)
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery
|
||||
|
||||
class AlbumArtistsQuery : ArtistsQuery()
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying;
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetAdditionalPartsRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data object GetSeriesTimersRequest
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetSpecialsRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class GetTrailersRequest(
|
||||
val itemId: UUID,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data class SpecialsQuery(
|
||||
val itemId: String,
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class StdItemQuery @JvmOverloads constructor(
|
||||
fields: Array<ItemFields> = defaultFields
|
||||
) : ItemQuery(), KoinComponent {
|
||||
companion object {
|
||||
val defaultFields = arrayOf(
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
userId = get<UserRepository>().currentUser.value!!.id.toString()
|
||||
setFields(fields)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data class TrailersQuery(
|
||||
val itemId: String,
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
package org.jellyfin.androidtv.data.querying
|
||||
|
||||
data object GetUserViewsRequest
|
||||
object ViewQuery
|
||||
@@ -6,7 +6,6 @@ import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.UUID
|
||||
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||
import java.time.Instant
|
||||
|
||||
interface ItemMutationRepository {
|
||||
suspend fun setFavorite(item: UUID, favorite: Boolean): UserItemDataDto
|
||||
@@ -23,7 +22,7 @@ class ItemMutationRepositoryImpl(
|
||||
else -> api.userLibraryApi.unmarkFavoriteItem(itemId = item)
|
||||
}
|
||||
|
||||
dataRefreshService.lastFavoriteUpdate = Instant.now()
|
||||
dataRefreshService.lastFavoriteUpdate = System.currentTimeMillis()
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.jellyfin.androidtv.data.repository
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -22,6 +23,7 @@ interface NotificationsRepository {
|
||||
|
||||
class NotificationsRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val uiModeManager: UiModeManager,
|
||||
private val systemPreferences: SystemPreferences,
|
||||
) : NotificationsRepository {
|
||||
override val notifications = MutableStateFlow(emptyList<AppNotification>())
|
||||
|
||||
@@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.flow
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.constant.CollectionType
|
||||
|
||||
interface UserViewsRepository {
|
||||
val views: Flow<Collection<BaseItemDto>>
|
||||
|
||||
fun isSupported(collectionType: CollectionType?): Boolean
|
||||
fun allowViewSelection(collectionType: CollectionType?): Boolean
|
||||
fun allowGridView(collectionType: CollectionType?): Boolean
|
||||
fun isSupported(collectionType: String?): Boolean
|
||||
fun allowViewSelection(collectionType: String?): Boolean
|
||||
fun allowGridView(collectionType: String?): Boolean
|
||||
}
|
||||
|
||||
class UserViewsRepositoryImpl(
|
||||
@@ -26,25 +26,25 @@ class UserViewsRepositoryImpl(
|
||||
emit(filteredViews)
|
||||
}
|
||||
|
||||
override fun isSupported(collectionType: CollectionType?) = collectionType !in unsupportedCollectionTypes
|
||||
override fun allowViewSelection(collectionType: CollectionType?) = collectionType !in disallowViewSelectionCollectionTypes
|
||||
override fun allowGridView(collectionType: CollectionType?) = collectionType !in disallowGridViewCollectionTypes
|
||||
override fun isSupported(collectionType: String?) = collectionType !in unsupportedCollectionTypes
|
||||
override fun allowViewSelection(collectionType: String?) = collectionType !in disallowViewSelectionCollectionTypes
|
||||
override fun allowGridView(collectionType: String?) = collectionType !in disallowGridViewCollectionTypes
|
||||
|
||||
private companion object {
|
||||
private val unsupportedCollectionTypes = arrayOf(
|
||||
CollectionType.BOOKS,
|
||||
CollectionType.FOLDERS
|
||||
CollectionType.Books,
|
||||
CollectionType.Folders
|
||||
)
|
||||
|
||||
private val disallowViewSelectionCollectionTypes = arrayOf(
|
||||
CollectionType.LIVETV,
|
||||
CollectionType.MUSIC,
|
||||
CollectionType.PHOTOS,
|
||||
CollectionType.LiveTv,
|
||||
CollectionType.Music,
|
||||
CollectionType.Photos,
|
||||
)
|
||||
|
||||
private val disallowGridViewCollectionTypes = arrayOf(
|
||||
CollectionType.LIVETV,
|
||||
CollectionType.MUSIC
|
||||
CollectionType.LiveTv,
|
||||
CollectionType.Music
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -48,10 +47,8 @@ class BackgroundService(
|
||||
private var _backgrounds = emptyList<ImageBitmap>()
|
||||
private var _currentIndex = 0
|
||||
private var _currentBackground = MutableStateFlow<ImageBitmap?>(null)
|
||||
private var _blurBackground = MutableStateFlow(false)
|
||||
private var _enabled = MutableStateFlow(true)
|
||||
val currentBackground get() = _currentBackground.asStateFlow()
|
||||
val blurBackground get() = _blurBackground.asStateFlow()
|
||||
val enabled get() = _enabled.asStateFlow()
|
||||
|
||||
// Helper function for [setBackground]
|
||||
@@ -79,9 +76,6 @@ class BackgroundService(
|
||||
if (baseItem == null || !userPreferences[UserPreferences.backdropEnabled])
|
||||
return clearBackgrounds()
|
||||
|
||||
// Enable blur for backdrops
|
||||
_blurBackground.value = true
|
||||
|
||||
// Get all backdrop urls
|
||||
val itemBackdropUrls = baseItem.backdropImageTags.getUrls(baseItem.id)
|
||||
val parentBackdropUrls = baseItem.parentBackdropImageTags.getUrls(baseItem.parentBackdropItemId)
|
||||
@@ -102,9 +96,6 @@ class BackgroundService(
|
||||
if (!server.splashscreenEnabled)
|
||||
return clearBackgrounds()
|
||||
|
||||
// Disable blur on splashscreen
|
||||
_blurBackground.value = false
|
||||
|
||||
// Manually grab the backdrop URL
|
||||
val api = jellyfin.createApi(baseUrl = server.address)
|
||||
val splashscreenUrl = api.imageApi.getSplashscreenUrl()
|
||||
@@ -153,7 +144,7 @@ class BackgroundService(
|
||||
}
|
||||
|
||||
internal fun update() {
|
||||
val now = Instant.now().toEpochMilli()
|
||||
val now = System.currentTimeMillis()
|
||||
if (lastBackgroundTimerUpdate > now - TRANSITION_DURATION.inWholeMilliseconds)
|
||||
return setTimer((lastBackgroundTimerUpdate - now).milliseconds + TRANSITION_DURATION, false)
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@ import org.jellyfin.androidtv.data.repository.NotificationsRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepository
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepositoryImpl
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.jellyfin.androidtv.integration.dream.DreamViewModel
|
||||
import org.jellyfin.androidtv.ui.ScreensaverViewModel
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepositoryImpl
|
||||
@@ -37,16 +35,13 @@ import org.jellyfin.androidtv.ui.search.SearchViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.ServerAddViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.StartupViewModel
|
||||
import org.jellyfin.androidtv.ui.startup.UserLoginViewModel
|
||||
import org.jellyfin.androidtv.util.KeyProcessor
|
||||
import org.jellyfin.androidtv.util.MarkdownRenderer
|
||||
import org.jellyfin.androidtv.util.PlaybackHelper
|
||||
import org.jellyfin.androidtv.util.apiclient.ReportingHelper
|
||||
import org.jellyfin.androidtv.util.sdk.SdkPlaybackHelper
|
||||
import org.jellyfin.androidtv.util.sdk.legacy
|
||||
import org.jellyfin.apiclient.AppInfo
|
||||
import org.jellyfin.apiclient.android
|
||||
import org.jellyfin.apiclient.logging.AndroidLogger
|
||||
import org.jellyfin.sdk.android.androidDevice
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.DeviceInfo
|
||||
@@ -81,7 +76,9 @@ val appModule = module {
|
||||
get<JellyfinSdk>().createApi()
|
||||
}
|
||||
|
||||
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
single { get<ApiClient>().ws() }
|
||||
|
||||
single { SocketHandler(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
||||
// Old apiclient
|
||||
single {
|
||||
@@ -115,7 +112,7 @@ val appModule = module {
|
||||
|
||||
single<UserRepository> { UserRepositoryImpl() }
|
||||
single<UserViewsRepository> { UserViewsRepositoryImpl(get()) }
|
||||
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get()) }
|
||||
single<NotificationsRepository> { NotificationsRepositoryImpl(get(), get(), get()) }
|
||||
single<ItemMutationRepository> { ItemMutationRepositoryImpl(get(), get()) }
|
||||
single<CustomMessageRepository> { CustomMessageRepositoryImpl() }
|
||||
single<NavigationRepository> { NavigationRepositoryImpl(Destinations.home) }
|
||||
@@ -128,15 +125,10 @@ val appModule = module {
|
||||
viewModel { PictureViewerViewModel(get()) }
|
||||
viewModel { ScreensaverViewModel(get()) }
|
||||
viewModel { SearchViewModel(get()) }
|
||||
viewModel { DreamViewModel(get(), get(), get(), get(), get()) }
|
||||
|
||||
single { BackgroundService(get(), get(), get(), get(), get()) }
|
||||
|
||||
single { MarkdownRenderer(get()) }
|
||||
single { ItemLauncher() }
|
||||
single { KeyProcessor() }
|
||||
single { ReportingHelper() }
|
||||
single<PlaybackHelper> { SdkPlaybackHelper(get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
||||
factory { (context: Context) -> SearchFragmentDelegate(context, get(), get()) }
|
||||
factory { (context: Context) -> SearchFragmentDelegate(context, get()) }
|
||||
}
|
||||
|
||||
@@ -12,17 +12,15 @@ import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.UserSettingPreferences
|
||||
import org.jellyfin.androidtv.ui.browsing.MainActivity
|
||||
import org.jellyfin.androidtv.ui.playback.GarbagePlaybackLauncher
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.LegacyMediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.RewritePlaybackLauncher
|
||||
import org.jellyfin.androidtv.ui.playback.VideoQueueManager
|
||||
import org.jellyfin.androidtv.ui.playback.rewrite.RewriteMediaManager
|
||||
import org.jellyfin.playback.core.mediasession.MediaSessionOptions
|
||||
import org.jellyfin.playback.core.mediasession.mediaSessionPlugin
|
||||
import org.jellyfin.playback.core.playbackManager
|
||||
import org.jellyfin.playback.exoplayer.exoPlayerPlugin
|
||||
import org.jellyfin.playback.jellyfin.jellyfinPlugin
|
||||
import org.jellyfin.playback.media3.exoplayer.ExoPlayerOptions
|
||||
import org.jellyfin.playback.media3.exoplayer.exoPlayerPlugin
|
||||
import org.jellyfin.playback.media3.session.MediaSessionOptions
|
||||
import org.jellyfin.playback.media3.session.media3SessionPlugin
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
@@ -32,7 +30,16 @@ import org.jellyfin.androidtv.ui.playback.PlaybackManager as LegacyPlaybackManag
|
||||
val playbackModule = module {
|
||||
single { LegacyPlaybackManager(get()) }
|
||||
single { VideoQueueManager() }
|
||||
single<MediaManager> { RewriteMediaManager(get(), get(), get(), get()) }
|
||||
single { LegacyMediaManager(get()) }
|
||||
single { RewriteMediaManager(get(), get(), get(), get()) }
|
||||
|
||||
factory {
|
||||
val preferences = get<UserPreferences>()
|
||||
val useRewrite = preferences[UserPreferences.playbackRewriteAudioEnabled]
|
||||
|
||||
if (useRewrite) get<RewriteMediaManager>()
|
||||
else get<LegacyMediaManager>()
|
||||
}
|
||||
|
||||
factory {
|
||||
val preferences = get<UserPreferences>()
|
||||
@@ -46,10 +53,13 @@ val playbackModule = module {
|
||||
}
|
||||
|
||||
fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
|
||||
install(exoPlayerPlugin(get()))
|
||||
install(jellyfinPlugin(get(), get()))
|
||||
|
||||
val activityIntent = Intent(get(), MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(get(), 0, activityIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val notificationChannelId = "session"
|
||||
val notificationChannelId = "mediasession"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
notificationChannelId,
|
||||
@@ -59,22 +69,12 @@ fun Scope.createPlaybackManager() = playbackManager(androidContext()) {
|
||||
NotificationManagerCompat.from(get()).createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val api = get<ApiClient>()
|
||||
val exoPlayerOptions = ExoPlayerOptions(
|
||||
httpConnectTimeout = api.httpClientOptions.connectTimeout,
|
||||
httpReadTimeout = api.httpClientOptions.requestTimeout
|
||||
)
|
||||
install(exoPlayerPlugin(get(), exoPlayerOptions))
|
||||
|
||||
val mediaSessionOptions = MediaSessionOptions(
|
||||
install(mediaSessionPlugin(get(), MediaSessionOptions(
|
||||
channelId = notificationChannelId,
|
||||
notificationId = 1,
|
||||
iconSmall = R.drawable.app_icon_foreground,
|
||||
openIntent = pendingIntent,
|
||||
)
|
||||
install(media3SessionPlugin(get(), mediaSessionOptions))
|
||||
|
||||
install(jellyfinPlugin(get()))
|
||||
)))
|
||||
|
||||
// Options
|
||||
val userSettingPreferences = get<UserSettingPreferences>()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.jellyfin.androidtv.di
|
||||
|
||||
import org.jellyfin.androidtv.util.AutoBitrate
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.koin.dsl.module
|
||||
|
||||
val utilsModule = module {
|
||||
single { AutoBitrate(get()) }
|
||||
single { ImageHelper(get()) }
|
||||
}
|
||||
|
||||
@@ -25,10 +25,9 @@ import org.jellyfin.androidtv.data.repository.UserViewsRepository
|
||||
import org.jellyfin.androidtv.integration.provider.ImageProvider
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.ui.startup.StartupActivity
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.dp
|
||||
import org.jellyfin.androidtv.util.sdk.isUsable
|
||||
import org.jellyfin.androidtv.util.stripHtml
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.exception.TimeoutException
|
||||
@@ -42,12 +41,11 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.constant.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import timber.log.Timber
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.time.Duration
|
||||
@@ -69,7 +67,6 @@ class LeanbackChannelWorker(
|
||||
private val api by inject<ApiClient>()
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val userViewsRepository by inject<UserViewsRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
/**
|
||||
* Check if the app can use Leanback features and is API level 26 or higher.
|
||||
@@ -259,7 +256,7 @@ class LeanbackChannelWorker(
|
||||
tag = imageTags?.get(ImageType.PRIMARY),
|
||||
)
|
||||
|
||||
else -> imageHelper.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
else -> ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
}.let(ImageProvider::getImageUri)
|
||||
|
||||
/**
|
||||
@@ -274,7 +271,7 @@ class LeanbackChannelWorker(
|
||||
fields = listOf(ItemFields.DATE_CREATED),
|
||||
imageTypeLimit = 1,
|
||||
limit = 10,
|
||||
mediaTypes = listOf(MediaType.VIDEO),
|
||||
mediaTypes = listOf(MediaType.Video),
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE, BaseItemKind.MOVIE),
|
||||
excludeActiveSessions = true,
|
||||
).content.items.orEmpty()
|
||||
@@ -282,9 +279,9 @@ class LeanbackChannelWorker(
|
||||
|
||||
val nextUp = async {
|
||||
api.tvShowsApi.getNextUp(
|
||||
userId = api.userId,
|
||||
imageTypeLimit = 1,
|
||||
limit = 10,
|
||||
enableResumable = false,
|
||||
fields = listOf(ItemFields.DATE_CREATED),
|
||||
).content.items.orEmpty()
|
||||
}
|
||||
@@ -364,7 +361,7 @@ class LeanbackChannelWorker(
|
||||
.setEpisodeTitle(if (item.type == BaseItemKind.EPISODE) item.name else null)
|
||||
.setSeasonNumber(seasonString, item.parentIndexNumber ?: 0)
|
||||
.setEpisodeNumber(episodeString, item.indexNumber ?: 0)
|
||||
.setDescription(item.overview?.stripHtml())
|
||||
.setDescription(item.overview)
|
||||
.setReleaseDate(
|
||||
if (item.premiereDate != null) DateTimeFormatter.ISO_DATE.format(item.premiereDate)
|
||||
else null
|
||||
@@ -458,7 +455,7 @@ class LeanbackChannelWorker(
|
||||
|
||||
setLastEngagementTimeUtcMillis(
|
||||
engagement?.toInstant(ZoneOffset.UTC)?.toEpochMilli()
|
||||
?: Instant.now().toEpochMilli()
|
||||
?: System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// Episode runtime has been determined
|
||||
|
||||
@@ -13,7 +13,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.jellyfin.androidtv.BuildConfig
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.integration.provider.ImageProvider
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.sdk.isUsable
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
@@ -41,7 +41,6 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
}
|
||||
|
||||
private val api by inject<ApiClient>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
override fun onCreate(): Boolean = api.isUsable
|
||||
|
||||
@@ -71,7 +70,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
* Gets the resumable items or returns null
|
||||
*/
|
||||
private suspend fun searchItems(query: String, limit: Int): BaseItemDtoQueryResult? = try {
|
||||
val items by api.itemsApi.getItems(
|
||||
val items by api.itemsApi.getItemsByUserId(
|
||||
searchTerm = query,
|
||||
recursive = true,
|
||||
limit = limit,
|
||||
@@ -107,7 +106,7 @@ class MediaContentProvider : ContentProvider(), KoinComponent {
|
||||
val imageUri = if (item.imageTags?.contains(ImageType.PRIMARY) == true)
|
||||
ImageProvider.getImageUri(api.imageApi.getItemImageUrl(item.id, ImageType.PRIMARY))
|
||||
else
|
||||
imageHelper.getResourceUrl(context!!, R.drawable.tile_land_tv)
|
||||
ImageUtils.getResourceUrl(context, R.drawable.tile_land_tv)
|
||||
|
||||
cursor.newRow().apply {
|
||||
add(BaseColumns._ID, item.id)
|
||||
|
||||
@@ -4,19 +4,15 @@ import android.service.dreams.DreamService
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
import androidx.savedstate.SavedStateRegistryController
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
|
||||
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner, ViewModelStoreOwner {
|
||||
abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner {
|
||||
@Suppress("LeakingThis")
|
||||
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||
|
||||
@@ -26,7 +22,6 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner, Vie
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle get() = lifecycleRegistry
|
||||
override val viewModelStore = ViewModelStore()
|
||||
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
@CallSuper
|
||||
@@ -51,12 +46,9 @@ abstract class DreamServiceCompat : DreamService(), SavedStateRegistryOwner, Vie
|
||||
|
||||
fun setContent(content: @Composable () -> Unit) {
|
||||
val view = ComposeView(this)
|
||||
// Set composition strategy
|
||||
view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
|
||||
// Inject dependencies normally added by appcompat activities
|
||||
view.setViewTreeLifecycleOwner(this)
|
||||
view.setViewTreeViewModelStoreOwner(this)
|
||||
view.setViewTreeSavedStateRegistryOwner(this)
|
||||
|
||||
// Set content composable
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
package org.jellyfin.androidtv.integration.dream
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.ui.playback.AudioEventListener
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class DreamViewModel(
|
||||
private val api: ApiClient,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val context: Context,
|
||||
private val mediaManager: MediaManager,
|
||||
private val userPreferences: UserPreferences,
|
||||
) : ViewModel() {
|
||||
private val _mediaContent = callbackFlow {
|
||||
trySend(mediaManager.currentAudioItem)
|
||||
|
||||
val listener = object : AudioEventListener {
|
||||
override fun onPlaybackStateChange(
|
||||
newState: PlaybackController.PlaybackState,
|
||||
currentItem: BaseItemDto?
|
||||
) {
|
||||
trySend(currentItem)
|
||||
}
|
||||
|
||||
override fun onQueueStatusChanged(hasQueue: Boolean) {
|
||||
trySend(mediaManager.currentAudioItem)
|
||||
}
|
||||
}
|
||||
|
||||
mediaManager.addAudioEventListener(listener)
|
||||
awaitClose { mediaManager.removeAudioEventListener(listener) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { it?.let(DreamContent::NowPlaying) }
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
DreamContent.NowPlaying(mediaManager.currentAudioItem)
|
||||
)
|
||||
|
||||
private val _libraryContent = flow {
|
||||
// Load first library item after 2 seconds
|
||||
// to force the logo at the start of the screensaver
|
||||
emit(null)
|
||||
delay(2.seconds)
|
||||
|
||||
while (true) {
|
||||
val next = getRandomLibraryShowcase()
|
||||
if (next != null) {
|
||||
emit(next)
|
||||
delay(30.seconds)
|
||||
} else {
|
||||
delay(3.seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
val content = combine(_mediaContent, _libraryContent) { mediaContent, libraryContent ->
|
||||
mediaContent ?: libraryContent ?: DreamContent.Logo
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = _mediaContent.value ?: _libraryContent.value ?: DreamContent.Logo,
|
||||
)
|
||||
|
||||
private suspend fun getRandomLibraryShowcase(): DreamContent.LibraryShowcase? {
|
||||
val requireParentalRating = userPreferences[UserPreferences.screensaverAgeRatingRequired]
|
||||
val maxParentalRating = userPreferences[UserPreferences.screensaverAgeRatingMax]
|
||||
|
||||
try {
|
||||
val response by api.itemsApi.getItems(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
sortBy = listOf(ItemSortBy.RANDOM),
|
||||
limit = 5,
|
||||
imageTypes = listOf(ImageType.BACKDROP),
|
||||
maxOfficialRating = if (maxParentalRating == -1) null else maxParentalRating.toString(),
|
||||
hasParentalRating = if (requireParentalRating) true else null,
|
||||
)
|
||||
|
||||
val item = response.items?.firstOrNull { item ->
|
||||
!item.backdropImageTags.isNullOrEmpty()
|
||||
} ?: return null
|
||||
|
||||
Timber.i("Loading random library showcase item ${item.id}")
|
||||
|
||||
val backdropTag = item.backdropImageTags!!.randomOrNull()
|
||||
?: item.imageTags?.get(ImageType.BACKDROP)
|
||||
|
||||
val logoTag = item.imageTags?.get(ImageType.LOGO)
|
||||
|
||||
val backdropUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.BACKDROP,
|
||||
tag = backdropTag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val logoUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.LOGO,
|
||||
tag = logoTag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val (logo, backdrop) = withContext(Dispatchers.IO) {
|
||||
val logoDeferred = async {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(logoUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
val backdropDeferred = async {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(backdropUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
awaitAll(logoDeferred, backdropDeferred)
|
||||
}
|
||||
|
||||
if (backdrop == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return DreamContent.LibraryShowcase(item, backdrop, logo)
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -12,9 +12,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.ui.composable.ZoomBox
|
||||
import org.jellyfin.androidtv.ui.composable.overscan
|
||||
@@ -49,21 +47,12 @@ fun DreamContentLibraryShowcase(
|
||||
.align(Alignment.BottomStart)
|
||||
.overscan(),
|
||||
) {
|
||||
if (content.logo != null) {
|
||||
Image(
|
||||
bitmap = content.logo.asImageBitmap(),
|
||||
contentDescription = content.item.name,
|
||||
modifier = Modifier
|
||||
.height(75.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = content.item.name.orEmpty(),
|
||||
style = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 32.sp
|
||||
),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = content.item.name.orEmpty(),
|
||||
style = TextStyle(
|
||||
color = Color.White,
|
||||
fontSize = 32.sp
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -22,14 +24,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.ui.composable.AsyncImage
|
||||
import org.jellyfin.androidtv.ui.composable.blurHashPainter
|
||||
@@ -41,7 +42,7 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.rememberKoinInject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -51,8 +52,8 @@ fun DreamContentNowPlaying(
|
||||
) = Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val api = koinInject<ApiClient>()
|
||||
val mediaManager = koinInject<MediaManager>()
|
||||
val api = rememberKoinInject<ApiClient>()
|
||||
val mediaManager = rememberKoinInject<MediaManager>()
|
||||
val item = content.item ?: return@Box
|
||||
|
||||
val primaryImageTag = item.imageTags?.get(ImageType.PRIMARY)
|
||||
@@ -62,8 +63,7 @@ fun DreamContentNowPlaying(
|
||||
else -> null to null
|
||||
}
|
||||
|
||||
val imageBlurHash =
|
||||
imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
|
||||
val imageBlurHash = imageTag?.let { tag -> item.imageBlurHashes?.get(ImageType.PRIMARY)?.get(tag) }
|
||||
if (imageBlurHash != null) {
|
||||
Image(
|
||||
painter = blurHashPainter(imageBlurHash, IntSize(32, 32)),
|
||||
@@ -114,15 +114,11 @@ fun DreamContentNowPlaying(
|
||||
|
||||
Text(
|
||||
text = item.run {
|
||||
val artistNames = artists.orEmpty()
|
||||
val albumArtistNames = albumArtists?.mapNotNull { it.name }.orEmpty()
|
||||
|
||||
when {
|
||||
artistNames.isNotEmpty() -> artistNames
|
||||
albumArtistNames.isNotEmpty() -> albumArtistNames
|
||||
else -> listOfNotNull(albumArtist)
|
||||
}.joinToString(", ")
|
||||
},
|
||||
if (!artists.isNullOrEmpty()) return@run artists?.joinToString(", ")
|
||||
val albumArtistNames = albumArtists?.mapNotNull { it.name }
|
||||
if (!albumArtistNames.isNullOrEmpty()) return@run albumArtistNames.joinToString(", ")
|
||||
return@run albumArtist
|
||||
}.orEmpty(),
|
||||
style = TextStyle(
|
||||
color = Color(0.8f, 0.8f, 0.8f),
|
||||
fontSize = 18.sp,
|
||||
@@ -147,17 +143,12 @@ fun DreamContentNowPlaying(
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.drawWithContent {
|
||||
// Background
|
||||
drawRect(Color.White, alpha = 0.2f)
|
||||
// Foreground
|
||||
drawRect(Color.White, size = size.copy(width = size.width * progress))
|
||||
}
|
||||
LinearProgressIndicator(
|
||||
progress = progress,
|
||||
color = Color.White,
|
||||
backgroundColor = Color.White.copy(alpha = 0.2f),
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -19,7 +20,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.ui.composable.overscan
|
||||
import org.jellyfin.androidtv.ui.composable.rememberCurrentTime
|
||||
|
||||
@@ -1,25 +1,111 @@
|
||||
package org.jellyfin.androidtv.integration.dream.composable
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import org.jellyfin.androidtv.integration.dream.DreamViewModel
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import org.jellyfin.androidtv.ui.composable.rememberMediaItem
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageFormat
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.koin.compose.rememberKoinInject
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun DreamHost() {
|
||||
val viewModel = koinViewModel<DreamViewModel>()
|
||||
val userPreferences = koinInject<UserPreferences>()
|
||||
val content by viewModel.content.collectAsState()
|
||||
val api = rememberKoinInject<ApiClient>()
|
||||
val userPreferences = rememberKoinInject<UserPreferences>()
|
||||
val mediaManager = rememberKoinInject<MediaManager>()
|
||||
val imageLoader = rememberKoinInject<ImageLoader>()
|
||||
val context = LocalContext.current
|
||||
|
||||
var libraryShowcase by remember { mutableStateOf<DreamContent.LibraryShowcase?>(null) }
|
||||
val mediaItem by rememberMediaItem(mediaManager)
|
||||
|
||||
LaunchedEffect(true) {
|
||||
delay(2.seconds)
|
||||
|
||||
while (true) {
|
||||
libraryShowcase = getRandomLibraryShowcase(api, imageLoader, context)
|
||||
delay(30.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
DreamView(
|
||||
content = content,
|
||||
content = when {
|
||||
mediaItem != null -> DreamContent.NowPlaying(mediaItem)
|
||||
libraryShowcase != null -> libraryShowcase!!
|
||||
else -> DreamContent.Logo
|
||||
},
|
||||
showClock = when (userPreferences[UserPreferences.clockBehavior]) {
|
||||
ClockBehavior.ALWAYS, ClockBehavior.IN_MENUS -> true
|
||||
else -> false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getRandomLibraryShowcase(
|
||||
api: ApiClient,
|
||||
imageLoader: ImageLoader,
|
||||
context: Context
|
||||
): DreamContent.LibraryShowcase? {
|
||||
try {
|
||||
val response by api.itemsApi.getItemsByUserId(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
sortBy = listOf(ItemSortBy.Random),
|
||||
limit = 5,
|
||||
imageTypes = listOf(ImageType.BACKDROP),
|
||||
// TODO: Add preferences for these two settings
|
||||
maxOfficialRating = "PG-13",
|
||||
// hasParentalRating = true,
|
||||
)
|
||||
|
||||
val item = response.items?.firstOrNull { item ->
|
||||
!item.backdropImageTags.isNullOrEmpty()
|
||||
} ?: return null
|
||||
|
||||
Timber.i("Loading random library showcase item ${item.id}")
|
||||
|
||||
val tag = item.backdropImageTags!!.randomOrNull()
|
||||
?: item.imageTags?.get(ImageType.BACKDROP)
|
||||
|
||||
val backdropUrl = api.imageApi.getItemImageUrl(
|
||||
itemId = item.id,
|
||||
imageType = ImageType.BACKDROP,
|
||||
tag = tag,
|
||||
format = ImageFormat.WEBP,
|
||||
)
|
||||
|
||||
val backdrop = withContext(Dispatchers.IO) {
|
||||
imageLoader.execute(
|
||||
request = ImageRequest.Builder(context).data(backdropUrl).build()
|
||||
).drawable?.toBitmap()
|
||||
} ?: return null
|
||||
|
||||
return DreamContent.LibraryShowcase(item, backdrop)
|
||||
} catch (err: ApiClientException) {
|
||||
Timber.e(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jellyfin.androidtv.integration.dream.composable
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jellyfin.androidtv.integration.dream.model.DreamContent
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun DreamView(
|
||||
content: DreamContent,
|
||||
|
||||
@@ -5,6 +5,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
sealed interface DreamContent {
|
||||
data object Logo : DreamContent
|
||||
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap, val logo: Bitmap?) : DreamContent
|
||||
data class LibraryShowcase(val item: BaseItemDto, val backdrop: Bitmap) : DreamContent
|
||||
data class NowPlaying(val item: BaseItemDto?) : DreamContent
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import org.jellyfin.androidtv.constant.PosterSize
|
||||
import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.stringPreference
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
|
||||
class LibraryPreferences(
|
||||
displayPreferencesId: String,
|
||||
@@ -28,7 +29,7 @@ class LibraryPreferences(
|
||||
val filterUnwatchedOnly = booleanPreference("FilterUnwatchedOnly", false)
|
||||
|
||||
// Item sorting
|
||||
val sortBy = enumPreference("SortBy", ItemSortBy.SORT_NAME)
|
||||
val sortBy = stringPreference("SortBy", ItemSortBy.SortName)
|
||||
val sortOrder = enumPreference("SortOrder", SortOrder.ASCENDING)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.jellyfin.androidtv.preference.store.DisplayPreferencesStore
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.stringPreference
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
|
||||
class LiveTvPreferences(
|
||||
api: ApiClient,
|
||||
@@ -13,7 +13,7 @@ class LiveTvPreferences(
|
||||
api = api,
|
||||
) {
|
||||
companion object {
|
||||
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DATE_PLAYED.name)
|
||||
val channelOrder = stringPreference("livetv-channelorder", ItemSortBy.DatePlayed)
|
||||
val colorCodeGuide = booleanPreference("guide-colorcodedbackgrounds", false)
|
||||
val favsAtTop = booleanPreference("livetv-favoritechannelsattop", true)
|
||||
val showHDIndicator = booleanPreference("guide-indicator-hd", false)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.jellyfin.androidtv.preference
|
||||
|
||||
import android.content.Context
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.store.SharedPreferenceStore
|
||||
import org.jellyfin.preference.stringPreference
|
||||
|
||||
@@ -58,6 +60,11 @@ class SystemPreferences(context: Context) : SharedPreferenceStore(
|
||||
val liveTvGuideFilterSports = booleanPreference("guide_filter_sports", false)
|
||||
|
||||
// Other persistent variables
|
||||
/**
|
||||
* Chosen player for play with button. Changes every time user chooses a player with "play with" button.
|
||||
*/
|
||||
var chosenPlayer = enumPreference("chosen_player", PreferredVideoPlayer.VLC)
|
||||
|
||||
/**
|
||||
* The version name for the latest dismissed beta notification or empty if none.
|
||||
*/
|
||||
|
||||
@@ -4,12 +4,14 @@ import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.jellyfin.androidtv.preference.constant.AppTheme
|
||||
import org.jellyfin.androidtv.preference.constant.AudioBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
|
||||
import org.jellyfin.androidtv.preference.constant.RatingType
|
||||
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.defaultAudioBehavior
|
||||
import org.jellyfin.androidtv.util.DeviceUtils
|
||||
import org.jellyfin.preference.booleanPreference
|
||||
import org.jellyfin.preference.enumPreference
|
||||
import org.jellyfin.preference.intPreference
|
||||
@@ -28,6 +30,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* The value used for automatic detection in [maxBitrate].
|
||||
*/
|
||||
const val MAX_BITRATE_AUTO = "0"
|
||||
|
||||
/* Display */
|
||||
/**
|
||||
* Select the app theme
|
||||
@@ -51,7 +58,8 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
|
||||
/* Playback - General*/
|
||||
/**
|
||||
* Maximum bitrate in megabit for playback.
|
||||
* Maximum bitrate in megabit for playback. A value of [MAX_BITRATE_AUTO] is used when
|
||||
* the bitrate should be automatically detected.
|
||||
*/
|
||||
var maxBitrate = stringPreference("pref_max_bitrate", "100")
|
||||
|
||||
@@ -83,9 +91,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
|
||||
/* Playback - Video */
|
||||
/**
|
||||
* Whether to use an external playback application or not.
|
||||
* Preferred video player.
|
||||
*/
|
||||
var useExternalPlayer = booleanPreference("external_player", false)
|
||||
var videoPlayer = enumPreference("video_player", PreferredVideoPlayer.EXOPLAYER)
|
||||
|
||||
/**
|
||||
* Change refresh rate to match media when device supports it
|
||||
@@ -96,17 +104,32 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
/**
|
||||
* Preferred behavior for audio streaming.
|
||||
*/
|
||||
var audioBehaviour = enumPreference("audio_behavior", AudioBehavior.DIRECT_STREAM)
|
||||
var audioBehaviour = enumPreference("audio_behavior", defaultAudioBehavior)
|
||||
|
||||
/**
|
||||
* Preferred behavior for audio streaming.
|
||||
*/
|
||||
var audioNightMode = enumPreference("audio_night_mode", false)
|
||||
|
||||
/**
|
||||
* Enable DTS
|
||||
*/
|
||||
var dtsEnabled = booleanPreference("pref_bitstream_dts", false)
|
||||
|
||||
/**
|
||||
* Enable AC3
|
||||
*/
|
||||
var ac3Enabled = booleanPreference("pref_bitstream_ac3", true)
|
||||
var ac3Enabled = booleanPreference("pref_bitstream_ac3", !DeviceUtils.isFireTvStickGen1)
|
||||
|
||||
/**
|
||||
* Default audio delay in milliseconds for libVLC
|
||||
*/
|
||||
var libVLCAudioDelay = intPreference("libvlc_audio_delay", 0)
|
||||
|
||||
/**
|
||||
* Use playback rewrite module for audio
|
||||
*/
|
||||
var playbackRewriteAudioEnabled = booleanPreference("playback_new_audio", true)
|
||||
|
||||
/* Live TV */
|
||||
/**
|
||||
@@ -114,6 +137,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
*/
|
||||
var liveTvDirectPlayEnabled = booleanPreference("pref_live_direct", true)
|
||||
|
||||
/**
|
||||
* Preferred video player for live TV
|
||||
*/
|
||||
var liveTvVideoPlayer = enumPreference("live_tv_video_player", PreferredVideoPlayer.AUTO)
|
||||
|
||||
/**
|
||||
* Shortcut used for changing the audio track
|
||||
*/
|
||||
@@ -191,14 +219,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
|
||||
var screensaverInAppTimeout = longPreference("screensaver_inapp_timeout", 5.minutes.inWholeMilliseconds)
|
||||
|
||||
/**
|
||||
* Age rating used to filter items in the screensaver. Use -1 to disable (omits parameter from requests).
|
||||
* Enable reactive homepage
|
||||
*/
|
||||
var screensaverAgeRatingMax = intPreference("screensaver_agerating_max", 13)
|
||||
|
||||
/**
|
||||
* Whether items shown in the screensaver are required to have an age rating set.
|
||||
*/
|
||||
var screensaverAgeRatingRequired = booleanPreference("screensaver_agerating_required", true)
|
||||
var homeReactive = booleanPreference("home_reactive", false)
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.util.DeviceUtils
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
|
||||
enum class AudioBehavior(
|
||||
@@ -16,3 +17,6 @@ enum class AudioBehavior(
|
||||
*/
|
||||
DOWNMIX_TO_STEREO(R.string.pref_audio_compat),
|
||||
}
|
||||
|
||||
val defaultAudioBehavior = if (DeviceUtils.isChromecastWithGoogleTV) AudioBehavior.DOWNMIX_TO_STEREO
|
||||
else AudioBehavior.DIRECT_STREAM
|
||||
|
||||
@@ -2,17 +2,17 @@ package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
|
||||
enum class LiveTvChannelOrder(
|
||||
override val nameRes: Int,
|
||||
val stringValue: String,
|
||||
) : PreferenceEnum {
|
||||
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DATE_PLAYED.serialName),
|
||||
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SORT_NAME.serialName);
|
||||
LAST_PLAYED(R.string.lbl_guide_option_played, ItemSortBy.DatePlayed),
|
||||
CHANNEL_NUMBER(R.string.lbl_guide_option_number, ItemSortBy.SortName);
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String) = entries
|
||||
fun fromString(value: String) = values()
|
||||
.firstOrNull { it.stringValue.equals(value, ignoreCase = true) }
|
||||
?: LAST_PLAYED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jellyfin.androidtv.preference.constant
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.preference.PreferenceEnum
|
||||
|
||||
enum class PreferredVideoPlayer(
|
||||
override val nameRes: Int,
|
||||
) : PreferenceEnum {
|
||||
/**
|
||||
* Automatically selects between exoplayer and vlc
|
||||
*/
|
||||
AUTO(R.string.pref_video_player_auto),
|
||||
|
||||
/**
|
||||
* Force ExoPlayer
|
||||
*/
|
||||
EXOPLAYER(R.string.pref_video_player_exoplayer),
|
||||
|
||||
/**
|
||||
* Force libVLC
|
||||
*/
|
||||
VLC(R.string.pref_video_player_vlc),
|
||||
|
||||
/**
|
||||
* Use external player
|
||||
*/
|
||||
EXTERNAL(R.string.pref_video_player_external),
|
||||
|
||||
/**
|
||||
* Choose a player - play with button
|
||||
*/
|
||||
CHOOSE(R.string.pref_video_player_choose),
|
||||
}
|
||||
@@ -19,7 +19,7 @@ enum class WatchedIndicatorBehavior(
|
||||
/**
|
||||
* Hide unwatched count indicator, show watched check mark on individual episodes only.
|
||||
*/
|
||||
EPISODES_ONLY(R.string.lbl_hide_watched_checkmark),
|
||||
EPISODES_ONLY(R.string.lbl_episode_marks),
|
||||
|
||||
/**
|
||||
* Never show watched indicators.
|
||||
|
||||
@@ -119,8 +119,8 @@ abstract class DisplayPreferencesStore(
|
||||
override fun <V : Enum<V>> setEnum(preference: Preference<*>, value: Enum<V>) =
|
||||
setString(
|
||||
preference.key, when (value) {
|
||||
is PreferenceEnum -> value.serializedName ?: value.name
|
||||
else -> value.name
|
||||
is PreferenceEnum -> value.serializedName ?: value.toString()
|
||||
else -> value.toString()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.util.BlurHashDecoder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.math.round
|
||||
@@ -65,7 +65,7 @@ class AsyncImageView @JvmOverloads constructor(
|
||||
|
||||
// Only show blurhash if an image is going to be loaded from the network
|
||||
if (url != null && blurHash != null) withContext(Dispatchers.IO) {
|
||||
val blurHashBitmap = BlurHashDecoder.decode(
|
||||
val blurHashBitmap = BlurHash.decode(
|
||||
blurHash,
|
||||
if (aspectRatio > 1) round(blurHashResolution * aspectRatio).toInt() else blurHashResolution,
|
||||
if (aspectRatio >= 1) blurHashResolution else round(blurHashResolution / aspectRatio).toInt(),
|
||||
|
||||
@@ -11,9 +11,7 @@ import org.jellyfin.androidtv.auth.repository.UserRepository
|
||||
import org.jellyfin.androidtv.databinding.ClockUserBugBinding
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@@ -26,8 +24,6 @@ class ClockUserView @JvmOverloads constructor(
|
||||
private val binding: ClockUserBugBinding = ClockUserBugBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val userRepository by inject<UserRepository>()
|
||||
private val navigationRepository by inject<NavigationRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
|
||||
var isVideoPlayer = false
|
||||
set(value) {
|
||||
@@ -35,23 +31,17 @@ class ClockUserView @JvmOverloads constructor(
|
||||
updateClockVisibility()
|
||||
}
|
||||
|
||||
val homeButton get() = binding.home
|
||||
|
||||
init {
|
||||
updateClockVisibility()
|
||||
|
||||
val currentUser = userRepository.currentUser.value
|
||||
|
||||
binding.clockUserImage.load(
|
||||
url = currentUser?.let(imageHelper::getPrimaryImageUrl),
|
||||
url = currentUser?.let(ImageUtils::getPrimaryImageUrl),
|
||||
placeholder = ContextCompat.getDrawable(context, R.drawable.ic_user)
|
||||
)
|
||||
|
||||
binding.clockUserImage.isVisible = currentUser != null
|
||||
|
||||
binding.home.setOnClickListener {
|
||||
navigationRepository.reset(Destinations.home, clearHistory = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateClockVisibility() {
|
||||
@@ -63,7 +53,5 @@ class ClockUserView @JvmOverloads constructor(
|
||||
ClockBehavior.IN_VIDEO -> isVideoPlayer
|
||||
ClockBehavior.IN_MENUS -> !isVideoPlayer
|
||||
}
|
||||
|
||||
binding.home.isVisible = !isVideoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@ import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
public class FriendlyDateButton extends FrameLayout {
|
||||
private LocalDateTime dateVal;
|
||||
private long dateVal;
|
||||
|
||||
public FriendlyDateButton(Context context, LocalDateTime thisDate, OnClickListener listener) {
|
||||
public FriendlyDateButton(Context context, long thisDate, OnClickListener listener) {
|
||||
super(context);
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
@@ -26,13 +25,14 @@ public class FriendlyDateButton extends FrameLayout {
|
||||
setOnClickListener(listener);
|
||||
|
||||
dateVal = thisDate;
|
||||
Date date = new Date(thisDate);
|
||||
|
||||
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, thisDate, true));
|
||||
((TextView)v.findViewById(R.id.date)).setText(DateTimeExtensionsKt.getDateFormatter(context).format(thisDate));
|
||||
((TextView)v.findViewById(R.id.friendlyName)).setText(TimeUtils.getFriendlyDate(context, date, true));
|
||||
((TextView)v.findViewById(R.id.date)).setText(android.text.format.DateFormat.getDateFormat(context).format(date));
|
||||
|
||||
}
|
||||
|
||||
public LocalDateTime getDate() { return dateVal; }
|
||||
public long getDate() { return dateVal; }
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||
|
||||
@@ -14,24 +14,25 @@ import androidx.core.content.ContextCompat;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuideFragment;
|
||||
import org.jellyfin.androidtv.util.ImageHelper;
|
||||
import org.jellyfin.androidtv.util.ImageUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
public class GuideChannelHeader extends RelativeLayout {
|
||||
private AsyncImageView mChannelImage;
|
||||
private ImageView mFavImage;
|
||||
private BaseItemDto mChannel;
|
||||
private ChannelInfoDto mChannel;
|
||||
private Context mContext;
|
||||
private LiveTvGuide mTvGuide;
|
||||
|
||||
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
|
||||
public GuideChannelHeader(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
|
||||
super(context);
|
||||
initComponent(context, tvGuide, channel);
|
||||
}
|
||||
|
||||
private void initComponent(Context context, LiveTvGuide tvGuide, BaseItemDto channel) {
|
||||
private void initComponent(Context context, LiveTvGuide tvGuide, ChannelInfoDto channel) {
|
||||
mContext = context;
|
||||
mChannel = channel;
|
||||
mTvGuide = tvGuide;
|
||||
@@ -49,14 +50,13 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
mChannelImage = findViewById(R.id.channelImage);
|
||||
mFavImage = findViewById(R.id.favImage);
|
||||
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
|
||||
mFavImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void loadImage() {
|
||||
ImageHelper imageHelper = KoinJavaComponent.<ImageHelper>get(ImageHelper.class);
|
||||
mChannelImage.load(
|
||||
imageHelper.getPrimaryImageUrl(mChannel,null, ImageHelper.MAX_PRIMARY_IMAGE_HEIGHT),
|
||||
ImageUtils.getPrimaryImageUrl(mChannel, KoinJavaComponent.<ApiClient>get(ApiClient.class)),
|
||||
null,
|
||||
null,
|
||||
0.0,
|
||||
@@ -64,11 +64,10 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
);
|
||||
}
|
||||
|
||||
public BaseItemDto getChannel() { return mChannel; }
|
||||
public void setChannel(BaseItemDto channel) { mChannel = channel; }
|
||||
public ChannelInfoDto getChannel() { return mChannel; }
|
||||
|
||||
public void refreshFavorite() {
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().isFavorite())
|
||||
if (mChannel.getUserData() != null && mChannel.getUserData().getIsFavorite())
|
||||
mFavImage.setVisibility(View.VISIBLE);
|
||||
else
|
||||
mFavImage.setVisibility(View.GONE);
|
||||
@@ -86,4 +85,5 @@ public class GuideChannelHeader extends RelativeLayout {
|
||||
setBackground(ContextCompat.getDrawable(mContext, R.drawable.light_border));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,12 +46,19 @@ public class ItemListView extends FrameLayout {
|
||||
mItemIds.clear();
|
||||
}
|
||||
|
||||
public void addItems(List<BaseItemDto> items) {
|
||||
int i = 0;
|
||||
for (BaseItemDto item : items) {
|
||||
addItem(item, i++);
|
||||
}
|
||||
}
|
||||
|
||||
public void addItem(BaseItemDto item, int ndx) {
|
||||
mList.addView(new ItemRowView(mContext, item, ndx, mRowSelectedListener, mRowClickedListener));
|
||||
mItemIds.add(item.getId());
|
||||
}
|
||||
|
||||
public ItemRowView updatePlaying(UUID id) {
|
||||
public ItemRowView updatePlaying(String id) {
|
||||
//look through our song rows and update the playing indicator
|
||||
ItemRowView ret = null;
|
||||
for (int i = 0; i < mList.getChildCount(); i++) {
|
||||
|
||||
@@ -12,7 +12,7 @@ fun ItemListView.refresh() {
|
||||
val api by KoinJavaComponent.inject<ApiClient>(ApiClient::class.java)
|
||||
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
|
||||
val response by api.itemsApi.getItems(
|
||||
val response by api.itemsApi.getItemsByUserId(
|
||||
ids = mItemIds,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
|
||||
@@ -16,9 +16,7 @@ import org.jellyfin.androidtv.databinding.ItemRowBinding;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.MediaType;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.jellyfin.sdk.model.constant.MediaType;
|
||||
|
||||
public class ItemRowView extends FrameLayout {
|
||||
Context mContext;
|
||||
@@ -120,7 +118,7 @@ public class ItemRowView extends FrameLayout {
|
||||
|
||||
public void updateWatched() {
|
||||
if (mBaseItem == null) return;
|
||||
if (MediaType.VIDEO.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
|
||||
if (MediaType.Video.equals(mBaseItem.getMediaType()) && mBaseItem.getUserData() != null && mBaseItem.getUserData().getPlayed()) {
|
||||
mWatchedMark.setText("✓");
|
||||
} else {
|
||||
mWatchedMark.setText("");
|
||||
@@ -151,7 +149,7 @@ public class ItemRowView extends FrameLayout {
|
||||
return playing;
|
||||
}
|
||||
|
||||
public boolean setPlaying(UUID id) {
|
||||
public boolean setPlaying(String id) {
|
||||
return setPlaying(getItem().getId().equals(id));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.jellyfin.androidtv.ui;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@@ -15,28 +18,36 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewTreeLifecycleOwner;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
|
||||
import org.jellyfin.apiclient.model.livetv.ChannelInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LiveProgramDetailPopup {
|
||||
private PopupWindow mPopup;
|
||||
private BaseItemDto mProgram;
|
||||
private ProgramGridCell mSelectedProgramView;
|
||||
final Context mContext;
|
||||
final Lifecycle lifecycle;
|
||||
private Activity mActivity;
|
||||
private final Lifecycle lifecycle;
|
||||
private LiveTvGuide mTvGuide;
|
||||
private TextView mDTitle;
|
||||
private TextView mDSummary;
|
||||
@@ -54,15 +65,17 @@ public class LiveProgramDetailPopup {
|
||||
private int mPosLeft;
|
||||
private int mPosTop;
|
||||
|
||||
public LiveProgramDetailPopup(Context context, LifecycleOwner lifecycleOwner, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
|
||||
mContext = context;
|
||||
lifecycle = lifecycleOwner.getLifecycle();
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
|
||||
public LiveProgramDetailPopup(Activity activity, Lifecycle lifecycle, LiveTvGuide tvGuide, int width, EmptyLifecycleAwareResponse tuneAction) {
|
||||
mActivity = activity;
|
||||
this.lifecycle = lifecycle;
|
||||
mTvGuide = tvGuide;
|
||||
mTuneAction = tuneAction;
|
||||
View layout = LayoutInflater.from(context).inflate(R.layout.program_detail_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(context, 400);
|
||||
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View layout = inflater.inflate(R.layout.program_detail_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(activity, 400);
|
||||
mPopup = new PopupWindow(layout, width, popupHeight);
|
||||
ViewTreeLifecycleOwner.set(mPopup.getContentView(), lifecycleOwner);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
@@ -83,127 +96,216 @@ public class LiveProgramDetailPopup {
|
||||
public void setContent(final BaseItemDto program, final ProgramGridCell selectedGridView) {
|
||||
mProgram = program;
|
||||
mSelectedProgramView = selectedGridView;
|
||||
mDTitle.setText(mProgram.getName());
|
||||
mDTitle.setText(program.getName());
|
||||
mDButtonRow.removeAllViews();
|
||||
if (program.getId() == null) {
|
||||
//empty item, just offer tune button
|
||||
mFirstButton = createTuneButton();
|
||||
createFavoriteButton();
|
||||
mDInfoRow.removeAllViews();
|
||||
mDTimeline.removeAllViews();
|
||||
mDSummary.setText("");
|
||||
return;
|
||||
}
|
||||
|
||||
mDSummary.setText(mProgram.getOverview());
|
||||
mDSummary.setText(program.getOverview());
|
||||
if (mDSummary.getLineCount() < 2) {
|
||||
mDSummary.setGravity(Gravity.CENTER);
|
||||
} else {
|
||||
mDSummary.setGravity(Gravity.START);
|
||||
mDSummary.setGravity(Gravity.LEFT);
|
||||
}
|
||||
|
||||
// build timeline info
|
||||
TvManager.setTimelineRow(mContext, mDTimeline, mProgram);
|
||||
TvManager.setTimelineRow(mActivity, mDTimeline, program);
|
||||
|
||||
//info row
|
||||
// TODO: Requires LifecycleOwner for compose
|
||||
// InfoLayoutHelper.addInfoRow(mContext, mProgram, mDInfoRow, false);
|
||||
InfoLayoutHelper.addInfoRow(mActivity, ModelCompat.asSdk(program), mDInfoRow, false, false);
|
||||
|
||||
//buttons
|
||||
mFirstButton = null;
|
||||
if (mProgram.getEndDate().isAfter(LocalDateTime.now())) {
|
||||
if (mProgram.getStartDate().isBefore(LocalDateTime.now())) {
|
||||
Date now = new Date();
|
||||
Date local = TimeUtils.convertToLocalDate(program.getStartDate());
|
||||
if (TimeUtils.convertToLocalDate(program.getEndDate()).getTime() > now.getTime()) {
|
||||
if (local.getTime() <= now.getTime()) {
|
||||
// program in progress - tune first button
|
||||
mFirstButton = createTuneButton();
|
||||
}
|
||||
|
||||
if (Utils.canManageRecordings(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue())) {
|
||||
if (mProgram.getTimerId() != null) {
|
||||
if (program.getTimerId() != null) {
|
||||
// cancel button
|
||||
Button cancel = new Button(mContext);
|
||||
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_recording));
|
||||
Button cancel = new Button(mActivity);
|
||||
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_recording));
|
||||
cancel.setTextColor(Color.WHITE);
|
||||
cancel.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LiveProgramDetailPopupHelperKt.cancelTimer(LiveProgramDetailPopup.this, mProgram.getTimerId(), () -> {
|
||||
selectedGridView.setRecTimer(null);
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, null);
|
||||
dismiss();
|
||||
Utils.showToast(mContext, R.string.msg_recording_cancelled);
|
||||
return null;
|
||||
apiClient.getValue().CancelLiveTvTimerAsync(program.getTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
selectedGridView.setRecTimer(null);
|
||||
program.setTimerId(null);
|
||||
dismiss();
|
||||
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(cancel);
|
||||
if (mFirstButton == null) mFirstButton = cancel;
|
||||
// recording info
|
||||
mDRecordInfo.setText(mProgram.getStartDate().isBefore(LocalDateTime.now()) ? mContext.getResources().getString(R.string.msg_recording_now) : mContext.getResources().getString(R.string.msg_will_record));
|
||||
mDRecordInfo.setText(local.getTime() <= now.getTime() ? mActivity.getResources().getString(R.string.msg_recording_now) : mActivity.getResources().getString(R.string.msg_will_record));
|
||||
} else {
|
||||
// record button
|
||||
Button rec = new Button(mContext);
|
||||
rec.setText(mContext.getResources().getString(R.string.lbl_record));
|
||||
Button rec = new Button(mActivity);
|
||||
rec.setText(mActivity.getResources().getString(R.string.lbl_record));
|
||||
rec.setTextColor(Color.WHITE);
|
||||
rec.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
rec.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LiveProgramDetailPopupHelperKt.recordProgram(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
|
||||
mProgram = program;
|
||||
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(program.getTimerId());
|
||||
if (mSeriesSettingsButton != null)
|
||||
mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
return null;
|
||||
//Create one-off recording with defaults
|
||||
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
apiClient.getValue().CreateLiveTvTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
mProgram = response;
|
||||
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(response.getTimerId());
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Timber.e(ex, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(rec);
|
||||
if (mFirstButton == null) mFirstButton = rec;
|
||||
mDRecordInfo.setText(mProgram.getSeriesTimerId() == null ? "" : mContext.getString(R.string.lbl_episode_not_record));
|
||||
mDRecordInfo.setText(program.getSeriesTimerId() == null ? "" : mActivity.getString(R.string.lbl_episode_not_record));
|
||||
}
|
||||
if (Utils.isTrue(mProgram.isSeries())) {
|
||||
if (mProgram.getSeriesTimerId() != null) {
|
||||
if (Utils.isTrue(program.getIsSeries())) {
|
||||
if (program.getSeriesTimerId() != null) {
|
||||
// cancel series button
|
||||
Button cancel = new Button(mContext);
|
||||
cancel.setText(mContext.getResources().getString(R.string.lbl_cancel_series));
|
||||
Button cancel = new Button(mActivity);
|
||||
cancel.setText(mActivity.getResources().getString(R.string.lbl_cancel_series));
|
||||
cancel.setTextColor(Color.WHITE);
|
||||
cancel.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getResources().getString(R.string.lbl_cancel_series))
|
||||
.setMessage(mContext.getResources().getString(R.string.msg_cancel_entire_series))
|
||||
new AlertDialog.Builder(mActivity)
|
||||
.setTitle(mActivity.getResources().getString(R.string.lbl_cancel_series))
|
||||
.setMessage(mActivity.getResources().getString(R.string.msg_cancel_entire_series))
|
||||
.setNegativeButton(R.string.lbl_no, null)
|
||||
.setPositiveButton(R.string.lbl_yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
LiveProgramDetailPopupHelperKt.cancelSeriesTimer(LiveProgramDetailPopup.this, mProgram.getSeriesTimerId(), () -> {
|
||||
selectedGridView.setRecSeriesTimer(null);
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, null);
|
||||
mSeriesSettingsButton.setVisibility(View.GONE);
|
||||
dismiss();
|
||||
Utils.showToast(mContext, R.string.msg_recording_cancelled);
|
||||
return null;
|
||||
apiClient.getValue().CancelLiveTvSeriesTimerAsync(program.getSeriesTimerId(), new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
selectedGridView.setRecSeriesTimer(null);
|
||||
program.setSeriesTimerId(null);
|
||||
mSeriesSettingsButton.setVisibility(View.GONE);
|
||||
dismiss();
|
||||
Utils.showToast(mActivity, R.string.msg_recording_cancelled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
});
|
||||
mDButtonRow.addView(cancel);
|
||||
} else {
|
||||
}else {
|
||||
// record series button
|
||||
Button rec = new Button(mContext);
|
||||
rec.setText(mContext.getResources().getString(R.string.lbl_record_series));
|
||||
Button rec = new Button(mActivity);
|
||||
rec.setText(mActivity.getResources().getString(R.string.lbl_record_series));
|
||||
rec.setTextColor(Color.WHITE);
|
||||
rec.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
rec.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LiveProgramDetailPopupHelperKt.recordSeries(LiveProgramDetailPopup.this, mProgram.getId(), program -> {
|
||||
mProgram = program;
|
||||
mSelectedProgramView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(program.getTimerId());
|
||||
if (mSeriesSettingsButton != null)
|
||||
mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
return null;
|
||||
//Create series recording with defaults
|
||||
apiClient.getValue().GetDefaultLiveTvTimerInfo(mProgram.getId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
apiClient.getValue().CreateLiveTvSeriesTimerAsync(response, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgram.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
mProgram = response;
|
||||
mSelectedProgramView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
mSelectedProgramView.setRecTimer(response.getTimerId());
|
||||
if (mSeriesSettingsButton != null) mSeriesSettingsButton.setVisibility(View.VISIBLE);
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Timber.e(ex, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Error creating recording");
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -211,8 +313,8 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
// manage series button
|
||||
mSeriesSettingsButton = new Button(mContext);
|
||||
mSeriesSettingsButton.setText(mContext.getResources().getString(R.string.lbl_series_settings));
|
||||
mSeriesSettingsButton = new Button(mActivity);
|
||||
mSeriesSettingsButton.setText(mActivity.getResources().getString(R.string.lbl_series_settings));
|
||||
mSeriesSettingsButton.setTextColor(Color.WHITE);
|
||||
mSeriesSettingsButton.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
mSeriesSettingsButton.setVisibility(mProgram.getSeriesTimerId() != null ? View.VISIBLE : View.GONE);
|
||||
@@ -228,18 +330,27 @@ public class LiveProgramDetailPopup {
|
||||
|
||||
}
|
||||
|
||||
if (mProgram.getStartDate().isAfter(LocalDateTime.now())) {
|
||||
if (local.getTime() > now.getTime()) {
|
||||
// add tune to button for programs that haven't started yet
|
||||
createTuneButton();
|
||||
}
|
||||
|
||||
createFavoriteButton();
|
||||
|
||||
|
||||
} else {
|
||||
// program has already ended
|
||||
mDRecordInfo.setText(mContext.getResources().getString(R.string.lbl_program_ended));
|
||||
mDRecordInfo.setText(mActivity.getResources().getString(R.string.lbl_program_ended));
|
||||
mFirstButton = createTuneButton();
|
||||
}
|
||||
// if (program.getIsMovie()) {
|
||||
// mDSimilarRow.setVisibility(View.VISIBLE);
|
||||
// mPopup.setHeight(MOVIE_HEIGHT);
|
||||
// } else {
|
||||
mDSimilarRow.setVisibility(View.GONE);
|
||||
// mPopup.setHeight(NORMAL_HEIGHT);
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
public Button createTuneButton() {
|
||||
@@ -256,19 +367,22 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
public android.widget.ImageButton createFavoriteButton() {
|
||||
BaseItemDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
|
||||
boolean isFav = channel.getUserData() != null && channel.getUserData().isFavorite();
|
||||
ChannelInfoDto channel = TvManager.getChannel(TvManager.getAllChannelsIndex(mProgram.getChannelId()));
|
||||
boolean isFav = channel.getUserData() != null && channel.getUserData().getIsFavorite();
|
||||
|
||||
android.widget.ImageButton fave = addImgButton(mDButtonRow, isFav ? R.drawable.ic_heart_red : R.drawable.ic_heart);
|
||||
fave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LiveProgramDetailPopupHelperKt.toggleFavorite(LiveProgramDetailPopup.this, channel, channel -> {
|
||||
fave.setImageDrawable(ContextCompat.getDrawable(mContext, channel.getUserData().isFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
|
||||
mTvGuide.refreshFavorite(channel.getId());
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
dataRefreshService.setLastFavoriteUpdate(Instant.now());
|
||||
return null;
|
||||
apiClient.getValue().UpdateFavoriteStatusAsync(channel.getId(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), !channel.getUserData().getIsFavorite(), new Response<UserItemDataDto>() {
|
||||
@Override
|
||||
public void onResponse(UserItemDataDto response) {
|
||||
channel.setUserData(response);
|
||||
fave.setImageDrawable(ContextCompat.getDrawable(mActivity, response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart));
|
||||
mTvGuide.refreshFavorite(channel.getId());
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
dataRefreshService.setLastFavoriteUpdate(System.currentTimeMillis());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -277,16 +391,16 @@ public class LiveProgramDetailPopup {
|
||||
}
|
||||
|
||||
private android.widget.ImageButton addImgButton(LinearLayout layout, int imgResource) {
|
||||
android.widget.ImageButton btn = new android.widget.ImageButton(mContext);
|
||||
btn.setImageDrawable(ContextCompat.getDrawable(mContext, imgResource));
|
||||
android.widget.ImageButton btn = new android.widget.ImageButton(mActivity);
|
||||
btn.setImageDrawable(ContextCompat.getDrawable(mActivity, imgResource));
|
||||
btn.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
layout.addView(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private Button addButton(LinearLayout layout, int stringResource) {
|
||||
Button btn = new Button(mContext);
|
||||
btn.setText(mContext.getResources().getString(stringResource));
|
||||
Button btn = new Button(mActivity);
|
||||
btn.setText(mActivity.getResources().getString(stringResource));
|
||||
btn.setTextColor(Color.WHITE);
|
||||
btn.setBackgroundResource(R.drawable.jellyfin_button);
|
||||
layout.addView(btn);
|
||||
@@ -310,13 +424,15 @@ public class LiveProgramDetailPopup {
|
||||
private RecordPopup mRecordPopup;
|
||||
|
||||
public void showRecordingOptions(final boolean recordSeries) {
|
||||
if (mRecordPopup == null)
|
||||
mRecordPopup = new RecordPopup(mContext, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
|
||||
|
||||
LiveProgramDetailPopupHelperKt.getSeriesTimer(this, mProgram.getSeriesTimerId(), seriesTimer -> {
|
||||
mRecordPopup.setContent(mContext, mProgram, seriesTimer, mSelectedProgramView, recordSeries);
|
||||
mRecordPopup.show();
|
||||
return null;
|
||||
if (mRecordPopup == null) mRecordPopup = new RecordPopup(mActivity, lifecycle, mAnchor, mPosLeft, mPosTop, mPopup.getWidth());
|
||||
apiClient.getValue().GetLiveTvSeriesTimerAsync(mProgram.getSeriesTimerId(), new Response<SeriesTimerInfoDto>() {
|
||||
@Override
|
||||
public void onResponse(SeriesTimerInfoDto response) {
|
||||
mRecordPopup.setContent(mActivity, ModelCompat.asSdk(mProgram), response, mSelectedProgramView, recordSeries);
|
||||
mRecordPopup.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
|
||||
import org.jellyfin.androidtv.util.getActivity
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
|
||||
fun BaseItemDto.copyWithTimerId(
|
||||
timerId: String?,
|
||||
) = copy(
|
||||
timerId = timerId,
|
||||
)
|
||||
|
||||
fun BaseItemDto.copyWithSeriesTimerId(
|
||||
seriesTimerId: String?,
|
||||
) = copy(
|
||||
seriesTimerId = seriesTimerId,
|
||||
)
|
||||
|
||||
fun LiveProgramDetailPopup.cancelTimer(
|
||||
timerId: String,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.cancelTimer(timerId)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.cancelSeriesTimer(
|
||||
seriesTimerId: String,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.cancelSeriesTimer(seriesTimerId)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun SeriesTimerInfoDto.asTimerInfoDto() = TimerInfoDto(
|
||||
id = id,
|
||||
type = type,
|
||||
serverId = serverId,
|
||||
externalId = externalId,
|
||||
channelId = channelId,
|
||||
externalChannelId = externalChannelId,
|
||||
channelName = channelName,
|
||||
channelPrimaryImageTag = channelPrimaryImageTag,
|
||||
programId = programId,
|
||||
externalProgramId = externalProgramId,
|
||||
name = name,
|
||||
overview = overview,
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
serviceName = serviceName,
|
||||
priority = priority,
|
||||
prePaddingSeconds = prePaddingSeconds,
|
||||
postPaddingSeconds = postPaddingSeconds,
|
||||
isPrePaddingRequired = isPrePaddingRequired,
|
||||
parentBackdropItemId = parentBackdropItemId,
|
||||
parentBackdropImageTags = parentBackdropImageTags,
|
||||
isPostPaddingRequired = isPostPaddingRequired,
|
||||
keepUntil = keepUntil,
|
||||
)
|
||||
|
||||
fun LiveProgramDetailPopup.recordProgram(
|
||||
programId: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val seriesTimer by api.liveTvApi.getDefaultTimer(programId.toString())
|
||||
val timer = seriesTimer.asTimerInfoDto()
|
||||
api.liveTvApi.createTimer(timer)
|
||||
api.liveTvApi.getProgram(programId.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.recordSeries(
|
||||
programId: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val timer by api.liveTvApi.getDefaultTimer(programId.toString())
|
||||
api.liveTvApi.createSeriesTimer(timer)
|
||||
api.liveTvApi.getProgram(programId.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.getSeriesTimer(
|
||||
seriesTimerId: String,
|
||||
callback: (seriesTimer: SeriesTimerInfoDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getSeriesTimer(seriesTimerId).content
|
||||
}.onSuccess { seriesTimer ->
|
||||
callback(seriesTimer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveProgramDetailPopup.toggleFavorite(
|
||||
item: BaseItemDto,
|
||||
callback: (item: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val itemMutationRepository by mContext.getActivity()!!.inject<ItemMutationRepository>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val userData = itemMutationRepository.setFavorite(
|
||||
item = item.id,
|
||||
favorite = !(item.userData?.isFavorite ?: false)
|
||||
)
|
||||
|
||||
item.copy(userData = userData)
|
||||
}.onSuccess { item ->
|
||||
callback(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import org.jellyfin.androidtv.ui.navigation.NavigationRepository
|
||||
import org.jellyfin.androidtv.ui.playback.AudioEventListener
|
||||
import org.jellyfin.androidtv.ui.playback.MediaManager
|
||||
import org.jellyfin.androidtv.ui.playback.PlaybackController
|
||||
import org.jellyfin.androidtv.util.ImageHelper
|
||||
import org.jellyfin.androidtv.util.ImageUtils
|
||||
import org.jellyfin.androidtv.util.TimeUtils
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
@@ -29,7 +29,6 @@ class NowPlayingView @JvmOverloads constructor(
|
||||
|
||||
private val mediaManager by inject<MediaManager>()
|
||||
private val navigationRepository by inject<NavigationRepository>()
|
||||
private val imageHelper by inject<ImageHelper>()
|
||||
private var currentDuration: String = ""
|
||||
|
||||
init {
|
||||
@@ -64,7 +63,7 @@ class NowPlayingView @JvmOverloads constructor(
|
||||
private fun setInfo(item: org.jellyfin.sdk.model.api.BaseItemDto) {
|
||||
val placeholder = ContextCompat.getDrawable(context, R.drawable.ic_album)
|
||||
val blurHash = item.imageBlurHashes?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY)?.get(item.imageTags?.get(org.jellyfin.sdk.model.api.ImageType.PRIMARY))
|
||||
binding.npIcon.load(imageHelper.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
|
||||
binding.npIcon.load(ImageUtils.getPrimaryImageUrl(item), blurHash, placeholder, item.primaryImageAspectRatio ?: 1.0)
|
||||
|
||||
currentDuration = TimeUtils.formatMillis(if (item.runTimeTicks != null) item.runTimeTicks!! / 10_000 else 0)
|
||||
binding.npDesc.text = if (item.albumArtist != null) item.albumArtist else item.name
|
||||
|
||||
@@ -17,12 +17,14 @@ import android.widget.TextView;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.preference.LiveTvPreferences;
|
||||
import org.jellyfin.androidtv.ui.livetv.LiveTvGuide;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.sdk.BaseItemExtensionsKt;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
public class ProgramGridCell extends RelativeLayout implements RecordingIndicatorView {
|
||||
|
||||
@@ -58,37 +60,37 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
setCellBackground();
|
||||
|
||||
if (program.getStartDate() != null && program.getEndDate() != null) {
|
||||
LocalDateTime localStart = program.getStartDate();
|
||||
if (localStart.plusMinutes(1).isBefore(activity.getCurrentLocalStartDate())) {
|
||||
Date localStart = TimeUtils.convertToLocalDate(program.getStartDate());
|
||||
if (localStart.getTime() + 60000 < activity.getCurrentLocalStartDate()) {
|
||||
mProgramName.setText("<< "+mProgramName.getText());
|
||||
TextView time = new TextView(context);
|
||||
time.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||
time.setTextSize(12);
|
||||
time.setText(DateTimeExtensionsKt.getTimeFormatter(getContext()).format(program.getStartDate()));
|
||||
time.setText(android.text.format.DateFormat.getTimeFormat(getContext()).format(TimeUtils.convertToLocalDate(program.getStartDate())));
|
||||
mInfoRow.addView(time);
|
||||
}
|
||||
}
|
||||
|
||||
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(program) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.isPremiere()))) {
|
||||
addBlockText(context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowNewIndicator()) && BaseItemExtensionsKt.isNew(ModelCompat.asSdk(program)) && (!liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) || !Utils.isTrue(program.getIsPremiere()))) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_new), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.isPremiere())) {
|
||||
addBlockText(context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowPremiereIndicator()) && Utils.isTrue(program.getIsPremiere())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_premiere), 10, Color.GRAY, R.drawable.dark_green_gradient);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.isRepeat())) {
|
||||
addBlockText(context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowRepeatIndicator()) && Utils.isTrue(program.getIsRepeat())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, context.getString(R.string.lbl_repeat), 10, Color.GRAY, androidx.leanback.R.color.lb_default_brand_color);
|
||||
}
|
||||
|
||||
if (program.getOfficialRating() != null && !program.getOfficialRating().equals("0")) {
|
||||
addBlockText(program.getOfficialRating(), 10, Color.BLACK, R.drawable.block_text_bg);
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, program.getOfficialRating(), 10);
|
||||
}
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.isHd())) {
|
||||
addBlockText("HD", 10, Color.BLACK, R.drawable.block_text_bg);
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getShowHDIndicator()) && Utils.isTrue(program.getIsHD())) {
|
||||
InfoLayoutHelper.addBlockText(context, mInfoRow, "HD", 10);
|
||||
}
|
||||
|
||||
if (program.getSeriesTimerId() != null) {
|
||||
@@ -109,29 +111,17 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
|
||||
}
|
||||
|
||||
private void addBlockText(String text, int size, int textColor, int backgroundRes) {
|
||||
TextView view = new TextView(getContext());
|
||||
view.setTextSize(size);
|
||||
view.setTextColor(textColor);
|
||||
view.setText(" " + text + " ");
|
||||
view.setBackgroundResource(backgroundRes);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
params.setMargins(0, Utils.convertDpToPixel(getContext(), -2), 0, 0);
|
||||
view.setLayoutParams(params);
|
||||
mInfoRow.addView(view);
|
||||
}
|
||||
|
||||
public void setCellBackground() {
|
||||
LiveTvPreferences liveTvPreferences = get(LiveTvPreferences.class);
|
||||
|
||||
if (liveTvPreferences.get(LiveTvPreferences.Companion.getColorCodeGuide())) {
|
||||
if (Utils.isTrue(mProgram.isMovie())) {
|
||||
if (Utils.isTrue(mProgram.getIsMovie())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_movie_bg);
|
||||
} else if (Utils.isTrue(mProgram.isNews())) {
|
||||
} else if (Utils.isTrue(mProgram.getIsNews())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_news_bg);
|
||||
} else if (Utils.isTrue(mProgram.isSports())) {
|
||||
} else if (Utils.isTrue(mProgram.getIsSports())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_sports_bg);
|
||||
} else if (Utils.isTrue(mProgram.isKids())) {
|
||||
} else if (Utils.isTrue(mProgram.getIsKids())) {
|
||||
mBackgroundColor = getResources().getColor(R.color.guide_kids_bg);
|
||||
}
|
||||
|
||||
@@ -160,13 +150,11 @@ public class ProgramGridCell extends RelativeLayout implements RecordingIndicato
|
||||
public boolean isFirst() { return isFirst; }
|
||||
|
||||
public void setRecTimer(String id) {
|
||||
if (mProgram == null) return;
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithTimerId(mProgram, id);
|
||||
mProgram.setTimerId(id);
|
||||
mRecIndicator.setImageResource(id != null ? (mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series_red : R.drawable.ic_record_red) : mProgram.getSeriesTimerId() != null ? R.drawable.ic_record_series : R.drawable.blank10x10);
|
||||
}
|
||||
public void setRecSeriesTimer(String id) {
|
||||
if (mProgram == null) return;
|
||||
mProgram = LiveProgramDetailPopupHelperKt.copyWithSeriesTimerId(mProgram, id);
|
||||
mProgram.setSeriesTimerId(id);
|
||||
mRecIndicator.setImageResource(id != null ? R.drawable.ic_record_series_red : R.drawable.blank10x10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
@@ -22,28 +23,29 @@ import android.widget.TextView;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.util.ContextExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.DateTimeExtensionsKt;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.interaction.Response;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.Date;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
public class RecordPopup {
|
||||
PopupWindow mPopup;
|
||||
UUID mProgramId;
|
||||
String mProgramId;
|
||||
SeriesTimerInfoDto mCurrentOptions;
|
||||
RecordingIndicatorView mSelectedView;
|
||||
View mAnchorView;
|
||||
@@ -51,8 +53,8 @@ public class RecordPopup {
|
||||
int mPosTop;
|
||||
boolean mRecordSeries;
|
||||
|
||||
Context mContext;
|
||||
final Lifecycle lifecycle;
|
||||
Activity mActivity;
|
||||
private final Lifecycle lifecycle;
|
||||
TextView mDTitle;
|
||||
LinearLayout mDTimeline;
|
||||
View mSeriesOptions;
|
||||
@@ -68,41 +70,43 @@ public class RecordPopup {
|
||||
ArrayList<String> mPaddingDisplayOptions;
|
||||
ArrayList<Integer> mPaddingValues = new ArrayList<>(Arrays.asList(0,60,300,900,1800,3600,5400,7200,10800));
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
private Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
|
||||
public RecordPopup(Context context, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
|
||||
mContext = context;
|
||||
public RecordPopup(Activity activity, Lifecycle lifecycle, View anchorView, int left, int top, int width) {
|
||||
mActivity = activity;
|
||||
this.lifecycle = lifecycle;
|
||||
mAnchorView = anchorView;
|
||||
mPosLeft = left;
|
||||
mPosTop = top;
|
||||
|
||||
mPaddingDisplayOptions = new ArrayList<>(Arrays.asList(
|
||||
context.getString(R.string.lbl_on_schedule),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 1),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 5),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 15),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 30),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 60),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.minutes, 90),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 2),
|
||||
ContextExtensionsKt.getQuantityString(context, R.plurals.hours, 3)
|
||||
mActivity.getString(R.string.lbl_on_schedule),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 1),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 5),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 15),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 30),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 60),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.minutes, 90),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 2),
|
||||
ContextExtensionsKt.getQuantityString(activity, R.plurals.hours, 3)
|
||||
));
|
||||
|
||||
View layout = LayoutInflater.from(context).inflate(R.layout.new_program_record_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(context, 330);
|
||||
LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View layout = inflater.inflate(R.layout.new_program_record_popup, null);
|
||||
int popupHeight = Utils.convertDpToPixel(activity, 330);
|
||||
mPopup = new PopupWindow(layout, width, popupHeight);
|
||||
mPopup.setFocusable(true);
|
||||
mPopup.setOutsideTouchable(true);
|
||||
mPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // necessary for popup to dismiss
|
||||
mDTitle = layout.findViewById(R.id.title);
|
||||
mDTitle = (TextView)layout.findViewById(R.id.title);
|
||||
|
||||
mPrePadding = layout.findViewById(R.id.prePadding);
|
||||
mPrePadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPrePadding = (Spinner) layout.findViewById(R.id.prePadding);
|
||||
mPrePadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPrePadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithPrePaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
|
||||
mCurrentOptions.setPrePaddingSeconds(mPaddingValues.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,11 +115,11 @@ public class RecordPopup {
|
||||
}
|
||||
});
|
||||
mPostPadding = (Spinner) layout.findViewById(R.id.postPadding);
|
||||
mPostPadding.setAdapter(new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPostPadding.setAdapter(new ArrayAdapter<>(mActivity, android.R.layout.simple_spinner_item, mPaddingDisplayOptions));
|
||||
mPostPadding.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithPostPaddingSeconds(mCurrentOptions, mPaddingValues.get(position));
|
||||
mCurrentOptions.setPostPaddingSeconds(mPaddingValues.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,39 +128,68 @@ public class RecordPopup {
|
||||
}
|
||||
});
|
||||
|
||||
mOnlyNew = layout.findViewById(R.id.onlyNew);
|
||||
mAnyChannel = layout.findViewById(R.id.anyChannel);
|
||||
mAnyTime = layout.findViewById(R.id.anyTime);
|
||||
mOnlyNew = (CheckBox) layout.findViewById(R.id.onlyNew);
|
||||
mAnyChannel = (CheckBox) layout.findViewById(R.id.anyChannel);
|
||||
mAnyTime = (CheckBox) layout.findViewById(R.id.anyTime);
|
||||
|
||||
mSeriesOptions = layout.findViewById(R.id.seriesOptions);
|
||||
|
||||
mOkButton = layout.findViewById(R.id.okButton);
|
||||
mOkButton = (Button) layout.findViewById(R.id.okButton);
|
||||
mOkButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mRecordSeries) {
|
||||
mCurrentOptions = RecordPopupHelperKt.copyWithFilters(mCurrentOptions, mOnlyNew.isChecked(), mAnyChannel.isChecked(), mAnyTime.isChecked());
|
||||
mCurrentOptions.setRecordNewOnly(mOnlyNew.isChecked());
|
||||
mCurrentOptions.setRecordAnyChannel(mAnyChannel.isChecked());
|
||||
mCurrentOptions.setRecordAnyTime(mAnyTime.isChecked());
|
||||
|
||||
apiClient.getValue().UpdateLiveTvSeriesTimerAsync(mCurrentOptions, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
Utils.showToast(mActivity, R.string.msg_settings_updated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
|
||||
RecordPopupHelperKt.updateSeriesTimer(RecordPopup.this, mCurrentOptions, () -> {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
Utils.showToast(mContext, R.string.msg_settings_updated);
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
TimerInfoDto updated = RecordPopupHelperKt.createProgramTimerInfo(mProgramId, mCurrentOptions);
|
||||
|
||||
RecordPopupHelperKt.updateTimer(RecordPopup.this, updated, () -> {
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
RecordPopupHelperKt.getLiveTvProgram(RecordPopup.this, mProgramId, program -> {
|
||||
mSelectedView.setRecTimer(program.getTimerId());
|
||||
mSelectedView.setRecSeriesTimer(program.getSeriesTimerId());
|
||||
return null;
|
||||
});
|
||||
Utils.showToast(mContext, R.string.msg_set_to_record);
|
||||
return null;
|
||||
} else {
|
||||
TimerInfoDto updated = new TimerInfoDto();
|
||||
updated.setProgramId(mProgramId);
|
||||
updated.setPrePaddingSeconds(mCurrentOptions.getPrePaddingSeconds());
|
||||
updated.setPostPaddingSeconds(mCurrentOptions.getPostPaddingSeconds());
|
||||
updated.setIsPrePaddingRequired(mCurrentOptions.getIsPrePaddingRequired());
|
||||
updated.setIsPostPaddingRequired(mCurrentOptions.getIsPostPaddingRequired());
|
||||
|
||||
apiClient.getValue().UpdateLiveTvTimerAsync(updated, new EmptyLifecycleAwareResponse(lifecycle) {
|
||||
@Override
|
||||
public void onResponse() {
|
||||
if (!getActive()) return;
|
||||
|
||||
mPopup.dismiss();
|
||||
customMessageRepository.getValue().pushMessage(CustomMessage.ActionComplete.INSTANCE);
|
||||
// we have to re-retrieve the program to get the timer id
|
||||
apiClient.getValue().GetLiveTvProgramAsync(mProgramId, KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<org.jellyfin.apiclient.model.dto.BaseItemDto>() {
|
||||
@Override
|
||||
public void onResponse(org.jellyfin.apiclient.model.dto.BaseItemDto response) {
|
||||
mSelectedView.setRecTimer(response.getTimerId());
|
||||
mSelectedView.setRecSeriesTimer(response.getSeriesTimerId());
|
||||
}
|
||||
});
|
||||
Utils.showToast(mActivity, R.string.msg_set_to_record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(mActivity, R.string.msg_unable_to_create_recording);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -178,7 +211,7 @@ public class RecordPopup {
|
||||
}
|
||||
|
||||
public void setContent(Context context, BaseItemDto program, SeriesTimerInfoDto current, RecordingIndicatorView selectedView, boolean recordSeries) {
|
||||
mProgramId = program.getId();
|
||||
mProgramId = program.getId().toString();
|
||||
mCurrentOptions = current;
|
||||
mRecordSeries = recordSeries;
|
||||
mSelectedView = selectedView;
|
||||
@@ -230,24 +263,17 @@ public class RecordPopup {
|
||||
timelineRow.removeAllViews();
|
||||
if (program.getStartDate() == null) return;
|
||||
|
||||
LocalDateTime local = program.getStartDate();
|
||||
TextView on = new TextView(mContext);
|
||||
on.setText(mContext.getString(R.string.lbl_on));
|
||||
Date local = TimeUtils.getDate(program.getStartDate());
|
||||
TextView on = new TextView(mActivity);
|
||||
on.setText(mActivity.getString(R.string.lbl_on));
|
||||
timelineRow.addView(on);
|
||||
TextView channel = new TextView(mContext);
|
||||
TextView channel = new TextView(mActivity);
|
||||
channel.setText(program.getChannelName());
|
||||
channel.setTypeface(null, Typeface.BOLD);
|
||||
channel.setTextColor(mContext.getResources().getColor(android.R.color.holo_blue_light));
|
||||
channel.setTextColor(mActivity.getResources().getColor(android.R.color.holo_blue_light));
|
||||
timelineRow.addView(channel);
|
||||
TextView datetime = new TextView(mContext);
|
||||
datetime.setText(new StringBuilder()
|
||||
.append(TimeUtils.getFriendlyDate(context, local))
|
||||
.append(" @ ")
|
||||
.append(DateTimeExtensionsKt.getTimeFormatter(mContext).format(local))
|
||||
.append(" (")
|
||||
.append(DateUtils.getRelativeTimeSpanString(local.toInstant(ZoneOffset.UTC).toEpochMilli(), Instant.now().toEpochMilli(), 0))
|
||||
.append(")")
|
||||
);
|
||||
TextView datetime = new TextView(mActivity);
|
||||
datetime.setText(TimeUtils.getFriendlyDate(context, local)+ " @ "+android.text.format.DateFormat.getTimeFormat(mActivity).format(local)+ " ("+ DateUtils.getRelativeTimeSpanString(local.getTime())+")");
|
||||
timelineRow.addView(datetime);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.androidtv.util.getActivity
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SeriesTimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.UUID
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithPrePaddingSeconds(
|
||||
prePaddingSeconds: Int,
|
||||
) = copy(
|
||||
prePaddingSeconds = prePaddingSeconds,
|
||||
)
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithPostPaddingSeconds(
|
||||
postPaddingSeconds: Int,
|
||||
) = copy(
|
||||
postPaddingSeconds = postPaddingSeconds,
|
||||
)
|
||||
|
||||
fun SeriesTimerInfoDto.copyWithFilters(
|
||||
recordNewOnly: Boolean,
|
||||
recordAnyChannel: Boolean,
|
||||
recordAnyTime: Boolean,
|
||||
) = copy(
|
||||
recordNewOnly = recordNewOnly,
|
||||
recordAnyChannel = recordAnyChannel,
|
||||
recordAnyTime = recordAnyTime,
|
||||
)
|
||||
|
||||
fun RecordPopup.updateSeriesTimer(
|
||||
seriesTimer: SeriesTimerInfoDto,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val id = seriesTimer.id
|
||||
if (id == null) api.liveTvApi.createSeriesTimer(seriesTimer)
|
||||
else api.liveTvApi.updateSeriesTimer(id, seriesTimer)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecordPopup.updateTimer(
|
||||
timer: TimerInfoDto,
|
||||
callback: () -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
val id = timer.id
|
||||
if (id == null) api.liveTvApi.createTimer(timer)
|
||||
else api.liveTvApi.updateTimer(id, timer)
|
||||
}.onSuccess {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createProgramTimerInfo(
|
||||
programId: UUID,
|
||||
options: SeriesTimerInfoDto,
|
||||
) = TimerInfoDto(
|
||||
programId = programId.toString(),
|
||||
prePaddingSeconds = options.prePaddingSeconds,
|
||||
postPaddingSeconds = options.postPaddingSeconds,
|
||||
isPrePaddingRequired = options.isPrePaddingRequired,
|
||||
isPostPaddingRequired = options.isPostPaddingRequired,
|
||||
)
|
||||
|
||||
fun RecordPopup.getLiveTvProgram(
|
||||
id: UUID,
|
||||
callback: (program: BaseItemDto) -> Unit,
|
||||
) {
|
||||
val api by mContext.getActivity()!!.inject<ApiClient>()
|
||||
|
||||
lifecycle.coroutineScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getProgram(id.toString()).content
|
||||
}.onSuccess { program ->
|
||||
callback(program)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/org/jellyfin/androidtv/ui/SettingsPopup.kt
Normal file
55
app/src/main/java/org/jellyfin/androidtv/ui/SettingsPopup.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package org.jellyfin.androidtv.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.isVisible
|
||||
import org.jellyfin.androidtv.databinding.SettingsPopupBinding
|
||||
import org.jellyfin.androidtv.util.dp
|
||||
|
||||
class SettingsPopup(
|
||||
private val context: Context,
|
||||
private val anchor: View,
|
||||
audioChangedListener: ValueChangedListener<Long>?,
|
||||
subtitleChangedListener: ValueChangedListener<Long>?
|
||||
) {
|
||||
private val binding = SettingsPopupBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
.apply {
|
||||
subtitleDelay.increment = SUBTITLE_DELAY_INCREMENT
|
||||
subtitleDelay.valueChangedListener = subtitleChangedListener
|
||||
audioDelay.valueChangedListener = audioChangedListener
|
||||
}
|
||||
|
||||
val popupWindow = PopupWindow(binding.root, WIDTH.dp(context), FULL_HEIGHT.dp(context))
|
||||
.apply {
|
||||
isFocusable = true
|
||||
isOutsideTouchable = true
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
|
||||
fun show(
|
||||
hasSubtitles: Boolean,
|
||||
audioDelay: Long,
|
||||
subtitleDelay: Long
|
||||
) {
|
||||
binding.audioDelay.value = audioDelay
|
||||
|
||||
binding.subtitleDelayTxt.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.isVisible = hasSubtitles
|
||||
binding.subtitleDelay.value = subtitleDelay
|
||||
|
||||
popupWindow.height = (if (hasSubtitles) FULL_HEIGHT else SINGLE_HEIGHT).dp(context)
|
||||
popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = 240
|
||||
private const val FULL_HEIGHT = 230
|
||||
private const val SINGLE_HEIGHT = 130
|
||||
private const val SUBTITLE_DELAY_INCREMENT = 500L
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui.background
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.snap
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -17,7 +18,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
@@ -25,11 +25,10 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.rememberKoinInject
|
||||
|
||||
@Composable
|
||||
private fun AppThemeBackground() {
|
||||
@@ -61,10 +60,10 @@ private fun AppThemeBackground() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
fun AppBackground() {
|
||||
val backgroundService = koinInject<BackgroundService>()
|
||||
val backgroundService = rememberKoinInject<BackgroundService>()
|
||||
val currentBackground by backgroundService.currentBackground.collectAsState()
|
||||
val blurBackground by backgroundService.blurBackground.collectAsState()
|
||||
val enabled by backgroundService.enabled.collectAsState()
|
||||
|
||||
if (enabled) {
|
||||
@@ -83,9 +82,7 @@ fun AppBackground() {
|
||||
alignment = Alignment.Center,
|
||||
contentScale = ContentScale.Crop,
|
||||
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (blurBackground) Modifier.blur(10.dp) else Modifier)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
AppThemeBackground()
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.leanback.app.BrowseSupportFragment
|
||||
import androidx.leanback.widget.HeaderItem
|
||||
import androidx.leanback.widget.ListRow
|
||||
@@ -16,12 +20,17 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.androidtv.constant.Extras
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService
|
||||
import org.jellyfin.androidtv.preference.UserPreferences
|
||||
import org.jellyfin.androidtv.preference.UserPreferences.Companion.clockBehavior
|
||||
import org.jellyfin.androidtv.preference.constant.ClockBehavior
|
||||
import org.jellyfin.androidtv.ui.ClockUserView
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter
|
||||
import org.jellyfin.androidtv.ui.presentation.PositionableListRowPresenter
|
||||
import org.jellyfin.androidtv.util.Utils.convertDpToPixel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@@ -31,8 +40,8 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
|
||||
protected var rows = mutableListOf<BrowseRowDef>()
|
||||
|
||||
private val userPreferences by inject<UserPreferences>()
|
||||
private val backgroundService by inject<BackgroundService>()
|
||||
private val itemLauncher by inject<ItemLauncher>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -48,9 +57,10 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
// Add event listeners
|
||||
onItemViewClickedListener = OnItemViewClickedListener { _: Presenter.ViewHolder?, item: Any?, _: RowPresenter.ViewHolder?, row: Row ->
|
||||
if (item is BaseRowItem) {
|
||||
itemLauncher.launch(
|
||||
ItemLauncher.launch(
|
||||
item,
|
||||
(row as ListRow).adapter as ItemRowAdapter,
|
||||
item.index,
|
||||
requireContext()
|
||||
)
|
||||
}
|
||||
@@ -60,7 +70,7 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
backgroundService.clearBackgrounds()
|
||||
} else {
|
||||
val adapter = (row as? ListRow)?.adapter
|
||||
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(adapter.indexOf(item))
|
||||
if (adapter is ItemRowAdapter) adapter.loadMoreItemsIfNeeded(item.index.toLong())
|
||||
|
||||
backgroundService.setBackground(item.baseItem)
|
||||
}
|
||||
@@ -102,4 +112,31 @@ abstract class BrowseFolderFragment : BrowseSupportFragment(), RowLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Check if clock should be visible
|
||||
val showClock = userPreferences[clockBehavior]
|
||||
if (showClock !== ClockBehavior.ALWAYS && showClock !== ClockBehavior.IN_MENUS) return
|
||||
|
||||
val root = requireActivity().findViewById<ViewGroup>(androidx.leanback.R.id.browse_frame)
|
||||
|
||||
// Move the title to the left to make way for the clock
|
||||
val titleView = root.findViewById<TextView>(androidx.leanback.R.id.title_text)
|
||||
if (titleView != null) {
|
||||
val layoutParams = titleView.layoutParams as FrameLayout.LayoutParams
|
||||
layoutParams.rightMargin = convertDpToPixel(root.context, 120)
|
||||
titleView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
// Add the clock element
|
||||
val layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val clockView = ClockUserView(root.context, null)
|
||||
layoutParams.gravity = Gravity.TOP or Gravity.END
|
||||
layoutParams.rightMargin = convertDpToPixel(root.context, 40)
|
||||
layoutParams.topMargin = convertDpToPixel(root.context, 20)
|
||||
clockView.layoutParams = layoutParams
|
||||
root.addView(clockView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ import org.jellyfin.androidtv.constant.ImageType;
|
||||
import org.jellyfin.androidtv.constant.PosterSize;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.FilterOptions;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.data.repository.UserViewsRepository;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
@@ -50,23 +52,24 @@ import org.jellyfin.androidtv.ui.AlphaPickerView;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapterHelperKt;
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations;
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.HorizontalGridPresenter;
|
||||
import org.jellyfin.androidtv.util.CoroutineUtils;
|
||||
import org.jellyfin.androidtv.util.ImageHelper;
|
||||
import org.jellyfin.androidtv.util.ImageUtils;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.androidtv.util.apiclient.EmptyLifecycleAwareResponse;
|
||||
import org.jellyfin.sdk.api.client.ApiClient;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.api.CollectionType;
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy;
|
||||
import org.jellyfin.sdk.model.api.SortOrder;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.HashMap;
|
||||
@@ -120,9 +123,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private final Lazy<UserRepository> userRepository = inject(UserRepository.class);
|
||||
private final Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
|
||||
private final Lazy<ItemLauncher> itemLauncher = inject(ItemLauncher.class);
|
||||
private final Lazy<KeyProcessor> keyProcessor = inject(KeyProcessor.class);
|
||||
private final Lazy<ApiClient> api = inject(ApiClient.class);
|
||||
|
||||
private int mCardsScreenEst = 0;
|
||||
private int mCardsScreenStride = 0;
|
||||
@@ -130,6 +130,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private final int MIN_NUM_CARDS = 5; // minimum number of visible cards we allow, this results in more empty space
|
||||
private final double CARD_SPACING_PCT = 1.0; // 100% expressed as relative to the padding_left/top, which depends on the mCardFocusScale and AspectRatio
|
||||
private final double CARD_SPACING_HORIZONTAL_BANNER_PCT = 0.5; // 50% allow horizontal card overlapping for banners, otherwise spacing is too large
|
||||
private final int MIN_GRIDSIZE_CHANGE_DELTA = 4; // minimum pixel size changes, to trigger a recreate of the grid via onGridSizeMeasurements
|
||||
private final int VIEW_SELECT_UPDATE_DELAY = 250; // delay in ms until we update the top-row info for a selected item
|
||||
|
||||
private boolean mDirty = true; // CardHeight, RowDef or GridSize changed
|
||||
@@ -144,6 +145,16 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
mGridHeight = Math.round(display.heightPixels / getResources().getDisplayMetrics().density - 130.6f);
|
||||
mGridWidth = Math.round(display.widthPixels / getResources().getDisplayMetrics().density);
|
||||
|
||||
sortOptions = new HashMap<>();
|
||||
{
|
||||
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SortName, SortOrder.ASCENDING));
|
||||
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DateCreated + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PremiereDate + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OfficialRating + "," + ItemSortBy.SortName, SortOrder.ASCENDING));
|
||||
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.CommunityRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CriticRating + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DatePlayed + "," + ItemSortBy.SortName, SortOrder.DESCENDING));
|
||||
}
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
@@ -161,21 +172,6 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
else
|
||||
setGridPresenter(new HorizontalGridPresenter());
|
||||
|
||||
sortOptions = new HashMap<>();
|
||||
{
|
||||
sortOptions.put(0, new SortOption(getString(R.string.lbl_name), ItemSortBy.SORT_NAME, SortOrder.ASCENDING));
|
||||
sortOptions.put(1, new SortOption(getString(R.string.lbl_date_added), ItemSortBy.DATE_CREATED, SortOrder.DESCENDING));
|
||||
sortOptions.put(2, new SortOption(getString(R.string.lbl_premier_date), ItemSortBy.PREMIERE_DATE, SortOrder.DESCENDING));
|
||||
sortOptions.put(3, new SortOption(getString(R.string.lbl_rating), ItemSortBy.OFFICIAL_RATING, SortOrder.ASCENDING));
|
||||
sortOptions.put(4, new SortOption(getString(R.string.lbl_community_rating), ItemSortBy.COMMUNITY_RATING, SortOrder.DESCENDING));
|
||||
sortOptions.put(5, new SortOption(getString(R.string.lbl_critic_rating), ItemSortBy.CRITIC_RATING, SortOrder.DESCENDING));
|
||||
sortOptions.put(6, new SortOption(getString(R.string.lbl_last_played), ItemSortBy.DATE_PLAYED, SortOrder.DESCENDING));
|
||||
|
||||
if (mFolder.getCollectionType() != null && mFolder.getCollectionType() == CollectionType.MOVIES) {
|
||||
sortOptions.put(7, new SortOption(getString(R.string.lbl_runtime), ItemSortBy.RUNTIME, SortOrder.ASCENDING));
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultGridRowCols(mPosterSizeSetting, mImageType);
|
||||
setAutoCardGridValues();
|
||||
setupQueries();
|
||||
@@ -206,7 +202,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() != KeyEvent.ACTION_UP) return false;
|
||||
return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, mActivity);
|
||||
return KeyProcessor.HandleKey(keyCode, mCurrentItem, mActivity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -276,7 +272,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
public void setItem(BaseRowItem item) {
|
||||
if (item != null) {
|
||||
binding.title.setText(item.getFullName(requireContext()));
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), item.getBaseItem(), binding.infoRow, true);
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), item, binding.infoRow, true, true);
|
||||
} else {
|
||||
binding.title.setText("");
|
||||
binding.infoRow.removeAllViews();
|
||||
@@ -285,10 +281,10 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
|
||||
public class SortOption {
|
||||
public String name;
|
||||
public ItemSortBy value;
|
||||
public String value;
|
||||
public SortOrder order;
|
||||
|
||||
public SortOption(String name, ItemSortBy value, SortOrder order) {
|
||||
public SortOption(String name, String value, SortOrder order) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.order = order;
|
||||
@@ -297,12 +293,13 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
|
||||
private Map<Integer, SortOption> sortOptions;
|
||||
|
||||
private SortOption getSortOption(ItemSortBy value) {
|
||||
for (SortOption sortOption : sortOptions.values()) {
|
||||
if (sortOption.value.equals(value)) return sortOption;
|
||||
private SortOption getSortOption(String value) {
|
||||
for (Integer key : sortOptions.keySet()) {
|
||||
SortOption option = sortOptions.get(key);
|
||||
if (Objects.requireNonNull(option).value.equals(value)) return option;
|
||||
}
|
||||
|
||||
return new SortOption(getString(R.string.lbl_bracket_unknown), ItemSortBy.SORT_NAME, SortOrder.ASCENDING);
|
||||
return new SortOption("Unknown", "", SortOrder.ASCENDING);
|
||||
}
|
||||
|
||||
public void setStatusText(String folderName) {
|
||||
@@ -362,15 +359,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
BaseItemKind fType = folder.getType();
|
||||
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
|
||||
return cardHeight;
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
|
||||
return cardHeight;
|
||||
} else {
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_2_3;
|
||||
return cardHeight * ImageUtils.ASPECT_RATIO_2_3;
|
||||
}
|
||||
case THUMB:
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_16_9;
|
||||
return cardHeight * ImageUtils.ASPECT_RATIO_16_9;
|
||||
case BANNER:
|
||||
return cardHeight * ImageHelper.ASPECT_RATIO_BANNER;
|
||||
return cardHeight * CardPresenter.ASPECT_RATIO_BANNER;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + imageType);
|
||||
}
|
||||
@@ -383,15 +380,15 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
BaseItemKind fType = folder.getType();
|
||||
if (fType == BaseItemKind.AUDIO || fType == BaseItemKind.GENRE || fType == BaseItemKind.MUSIC_ALBUM || fType == BaseItemKind.MUSIC_ARTIST || fType == BaseItemKind.MUSIC_GENRE) {
|
||||
return cardWidth;
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.MUSIC.equals(folder.getCollectionType())) {
|
||||
} else if (fType == BaseItemKind.COLLECTION_FOLDER && CollectionType.Music.equals(folder.getCollectionType())) {
|
||||
return cardWidth;
|
||||
} else {
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_2_3;
|
||||
return cardWidth / ImageUtils.ASPECT_RATIO_2_3;
|
||||
}
|
||||
case THUMB:
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_16_9;
|
||||
return cardWidth / ImageUtils.ASPECT_RATIO_16_9;
|
||||
case BANNER:
|
||||
return cardWidth / ImageHelper.ASPECT_RATIO_BANNER;
|
||||
return cardWidth / CardPresenter.ASPECT_RATIO_BANNER;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected value: " + imageType);
|
||||
}
|
||||
@@ -545,22 +542,63 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
}
|
||||
|
||||
private void setupQueries() {
|
||||
StdItemQuery query = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.DisplayPreferencesId
|
||||
});
|
||||
query.setParentId(mParentId.toString());
|
||||
if (mFolder.getType() == BaseItemKind.USER_VIEW || mFolder.getType() == BaseItemKind.COLLECTION_FOLDER) {
|
||||
CollectionType type = mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
|
||||
if (type == CollectionType.MUSIC) {
|
||||
//Special queries needed for album artists
|
||||
String includeType = getArguments().getString(Extras.IncludeType, null);
|
||||
if ("AlbumArtist".equals(includeType)) {
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createAlbumArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
} else if ("Artist".equals(includeType)) {
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createArtistsRequest(mParentId), CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
}
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
switch (type) {
|
||||
case CollectionType.Movies:
|
||||
query.setIncludeItemTypes(new String[]{"Movie"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.TvShows:
|
||||
query.setIncludeItemTypes(new String[]{"Series"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.BoxSets:
|
||||
query.setIncludeItemTypes(new String[]{"BoxSet"});
|
||||
query.setParentId(null);
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
case CollectionType.Music:
|
||||
//Special queries needed for album artists
|
||||
String includeType = getArguments().getString(Extras.IncludeType);
|
||||
if ("AlbumArtist".equals(includeType)) {
|
||||
AlbumArtistsQuery albumArtists = new AlbumArtistsQuery();
|
||||
albumArtists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
|
||||
albumArtists.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
albumArtists.setParentId(mParentId.toString());
|
||||
setRowDef(new BrowseRowDef("", albumArtists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
} else if ("Artist".equals(includeType)) {
|
||||
ArtistsQuery artists = new ArtistsQuery();
|
||||
artists.setUserId(userRepository.getValue().getCurrentUser().getValue().getId().toString());
|
||||
artists.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
artists.setParentId(mParentId.toString());
|
||||
setRowDef(new BrowseRowDef("", artists, CHUNK_SIZE_MINIMUM, new ChangeTriggerType[]{}));
|
||||
return;
|
||||
}
|
||||
query.setIncludeItemTypes(new String[]{includeType != null ? includeType : "MusicAlbum"});
|
||||
query.setRecursive(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setRowDef(new BrowseRowDef("", BrowsingUtils.createBrowseGridItemsRequest(mFolder), CHUNK_SIZE_MINIMUM, false, true));
|
||||
setRowDef(new BrowseRowDef("", query, CHUNK_SIZE_MINIMUM, false, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -571,6 +609,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
ImageType imageType = libraryPreferences.get(LibraryPreferences.Companion.getImageType());
|
||||
GridDirection gridDirection = libraryPreferences.get(LibraryPreferences.Companion.getGridDirection());
|
||||
|
||||
|
||||
if (mImageType != imageType || mPosterSizeSetting != posterSizeSetting || mGridDirection != gridDirection || mDirty) {
|
||||
determiningPosterSize = true;
|
||||
|
||||
@@ -626,8 +665,14 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
case NextUp:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getNextUpQuery(), true, mCardPresenter, null);
|
||||
break;
|
||||
case Season:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSeasonQuery(), mCardPresenter, null);
|
||||
break;
|
||||
case Upcoming:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getUpcomingQuery(), mCardPresenter, null);
|
||||
break;
|
||||
case Views:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), GetUserViewsRequest.INSTANCE, mCardPresenter, null);
|
||||
mAdapter = new ItemRowAdapter(requireContext(), ViewQuery.INSTANCE, mCardPresenter, null);
|
||||
break;
|
||||
case SimilarSeries:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, null);
|
||||
@@ -635,6 +680,9 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
case SimilarMovies:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, null);
|
||||
break;
|
||||
case Persons:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getPersonsQuery(), chunkSize, mCardPresenter, null);
|
||||
break;
|
||||
case LiveTvChannel:
|
||||
mAdapter = new ItemRowAdapter(requireContext(), mRowDef.getTvChannelQuery(), 40, mCardPresenter, null);
|
||||
break;
|
||||
@@ -670,7 +718,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
setItem(null);
|
||||
updateCounter(mAdapter.getTotalItems() > 0 ? 1 : 0);
|
||||
}
|
||||
mLetterButton.setVisibility(ItemSortBy.SORT_NAME.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
|
||||
mLetterButton.setVisibility(ItemSortBy.SortName.equals(mAdapter.getSortBy()) ? View.VISIBLE : View.GONE);
|
||||
if (mAdapter.getItemsLoaded() == 0) {
|
||||
mGridView.setFocusable(false);
|
||||
mHandler.postDelayed(() -> {
|
||||
@@ -706,13 +754,11 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
private ImageButton mLetterButton;
|
||||
|
||||
private void updateDisplayPrefs() {
|
||||
CoroutineUtils.runOnLifecycle(getLifecycle(), (coroutineScope, continuation) -> {
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
|
||||
return libraryPreferences.commit(continuation);
|
||||
});
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterFavoritesOnly(), mAdapter.getFilters().isFavoriteOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
|
||||
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
|
||||
CoroutineUtils.runOnLifecycle(getLifecycle(), (coroutineScope, continuation) -> libraryPreferences.commit(continuation));
|
||||
}
|
||||
|
||||
private void addTools() {
|
||||
@@ -728,9 +774,12 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
public void onClick(View v) {
|
||||
//Create sort menu
|
||||
PopupMenu sortMenu = new PopupMenu(getActivity(), binding.toolBar, Gravity.END);
|
||||
for (Map.Entry<Integer, SortOption> entry : sortOptions.entrySet()) {
|
||||
MenuItem item = sortMenu.getMenu().add(0, entry.getKey(), entry.getKey(), entry.getValue().name);
|
||||
item.setChecked(entry.getValue().value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())));
|
||||
for (Integer key : sortOptions.keySet()) {
|
||||
SortOption option = sortOptions.get(key);
|
||||
if (option == null) option = sortOptions.get(0);
|
||||
MenuItem item = sortMenu.getMenu().add(0, key, key, Objects.requireNonNull(option).name);
|
||||
if (option.value.equals(libraryPreferences.get(LibraryPreferences.Companion.getSortBy())))
|
||||
item.setChecked(true);
|
||||
}
|
||||
sortMenu.getMenu().setGroupCheckable(0, true, true);
|
||||
sortMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@@ -879,20 +928,31 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentItem == null) return;
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
ItemRowAdapterHelperKt.refreshItem(mAdapter, api.getValue(), this, mCurrentItem, () -> {
|
||||
//Now - if filtered make sure we still pass
|
||||
if (mAdapter.getFilters() == null) return null;
|
||||
if ((mAdapter.getFilters().isFavoriteOnly() && !mCurrentItem.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && mCurrentItem.isPlayed())) {
|
||||
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
|
||||
binding.toolBar.requestFocus();
|
||||
mAdapter.remove(mCurrentItem);
|
||||
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
|
||||
updateCounter(mAdapter.indexOf(mCurrentItem));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (mCurrentItem != null && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO && mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO_ALBUM
|
||||
&& mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ARTIST && mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ALBUM) {
|
||||
Timber.d("Refresh item \"%s\"", mCurrentItem.getFullName(requireContext()));
|
||||
BaseRowItem item = mCurrentItem;
|
||||
item.refresh(new LifecycleAwareResponse<BaseItemDto>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
if (response == null) mAdapter.removeAt(mAdapter.indexOf(item), 1);
|
||||
else mAdapter.notifyItemRangeChanged(mAdapter.indexOf(item), 1);
|
||||
|
||||
//Now - if filtered make sure we still pass
|
||||
if (response != null && mAdapter.getFilters() != null) {
|
||||
if ((mAdapter.getFilters().isFavoriteOnly() && !item.isFavorite()) || (mAdapter.getFilters().isUnwatchedOnly() && item.isPlayed())) {
|
||||
// if we are about to remove the current item, throw focus to toolbar so framework doesn't crash
|
||||
binding.toolBar.requestFocus();
|
||||
mAdapter.remove(item);
|
||||
mAdapter.setTotalItems(mAdapter.getTotalItems() - 1);
|
||||
updateCounter(item.getIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemViewClickedListener implements OnItemViewClickedListener {
|
||||
@@ -901,7 +961,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
itemLauncher.getValue().launch((BaseRowItem) item, mAdapter, requireContext());
|
||||
ItemLauncher.launch((BaseRowItem) item, mAdapter, ((BaseRowItem) item).getIndex(), requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,7 +992,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
|
||||
mHandler.postDelayed(mDelayedSetItem, VIEW_SELECT_UPDATE_DELAY);
|
||||
|
||||
if (!determiningPosterSize)
|
||||
mAdapter.loadMoreItemsIfNeeded(mAdapter.indexOf(mCurrentItem));
|
||||
mAdapter.loadMoreItemsIfNeeded(mCurrentItem.getIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,30 @@ import androidx.leanback.widget.Row;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.entities.LocationType;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
@@ -42,56 +54,125 @@ public class BrowseRecordingsFragment extends EnhancedBrowseFragment {
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
showViews = true;
|
||||
//Latest Recordings
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(40), 40));
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 40));
|
||||
|
||||
//Movies
|
||||
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), BrowsingUtils.createLiveTVMovieRecordingsRequest(), 60);
|
||||
RecordingQuery movies = new RecordingQuery();
|
||||
movies.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
movies.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
movies.setEnableImages(true);
|
||||
movies.setIsMovie(true);
|
||||
BrowseRowDef moviesDef = new BrowseRowDef(getString(R.string.lbl_movies), movies, 60);
|
||||
|
||||
//Shows
|
||||
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), BrowsingUtils.createLiveTVSeriesRecordingsRequest(), 60);
|
||||
RecordingQuery shows = new RecordingQuery();
|
||||
shows.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
shows.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
shows.setEnableImages(true);
|
||||
shows.setIsSeries(true);
|
||||
BrowseRowDef showsDef = new BrowseRowDef(getString(R.string.lbl_tv_series), shows, 60);
|
||||
|
||||
mRows.add(showsDef);
|
||||
mRows.add(moviesDef);
|
||||
|
||||
//Sports
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), BrowsingUtils.createLiveTVSportsRecordingsRequest(), 60));
|
||||
RecordingQuery sports = new RecordingQuery();
|
||||
sports.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
sports.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
sports.setEnableImages(true);
|
||||
sports.setIsSports(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_sports), sports, 60));
|
||||
|
||||
//Kids
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), BrowsingUtils.createLiveTVKidsRecordingsRequest(), 60));
|
||||
RecordingQuery kids = new RecordingQuery();
|
||||
kids.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
kids.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
kids.setEnableImages(true);
|
||||
kids.setIsKids(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_kids), kids, 60));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
addNext24Timers();
|
||||
}
|
||||
|
||||
private void addNext24Timers() {
|
||||
BrowseViewFragmentHelperKt.getLiveTvTimers(this, timers -> {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : timers.getItems()) {
|
||||
if (timer.getStartDate().isBefore(next24)) {
|
||||
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
final long ticks24 = 1000 * 60 * 60 * 24;
|
||||
KoinJavaComponent.<ApiClient>get(ApiClient.class).GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
long next24 = System.currentTimeMillis() + ticks24;
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : response.getItems()) {
|
||||
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
|
||||
BaseItemDto programInfo = timer.getProgramInfo();
|
||||
if (programInfo == null) {
|
||||
programInfo = new BaseItemDto();
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
programInfo.setStartDate(timer.getStartDate());
|
||||
programInfo.setEndDate(timer.getEndDate());
|
||||
}
|
||||
programInfo.setLocationType(LocationType.Virtual);
|
||||
nearTimers.add(programInfo);
|
||||
}
|
||||
}
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
|
||||
|
||||
mRowsFragment.setSelectedPosition(0, true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED))
|
||||
return;
|
||||
|
||||
mRowsFragment.setSelectedPosition(0, true);
|
||||
}
|
||||
}, 500);
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
}
|
||||
return null;
|
||||
}, exception -> {
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
return null;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,35 +2,41 @@ package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
|
||||
import org.jellyfin.androidtv.data.querying.GetSpecialsRequest;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetArtistsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest;
|
||||
import org.jellyfin.androidtv.data.querying.AlbumArtistsQuery;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ArtistsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery;
|
||||
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
import org.jellyfin.apiclient.model.querying.PersonsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.SeasonQuery;
|
||||
import org.jellyfin.apiclient.model.querying.SimilarItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.UpcomingEpisodesQuery;
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest;
|
||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest;
|
||||
|
||||
public class BrowseRowDef {
|
||||
private String headerText;
|
||||
private GetItemsRequest query;
|
||||
private GetNextUpRequest nextUpQuery;
|
||||
private GetSimilarItemsRequest similarQuery;
|
||||
private GetLatestMediaRequest latestItemsQuery;
|
||||
private GetLiveTvChannelsRequest tvChannelQuery;
|
||||
private GetRecommendedProgramsRequest programQuery;
|
||||
private GetRecordingsRequest recordingQuery;
|
||||
private GetSeriesTimersRequest seriesTimerQuery;
|
||||
private ItemQuery query;
|
||||
private NextUpQuery nextUpQuery;
|
||||
private UpcomingEpisodesQuery upcomingQuery;
|
||||
private SimilarItemsQuery similarQuery;
|
||||
private LatestItemsQuery latestItemsQuery;
|
||||
|
||||
private GetArtistsRequest artistsQuery;
|
||||
private GetAlbumArtistsRequest albumArtistsQuery;
|
||||
private PersonsQuery personsQuery;
|
||||
|
||||
private LiveTvChannelQuery tvChannelQuery;
|
||||
private RecommendedProgramQuery programQuery;
|
||||
private RecordingQuery recordingQuery;
|
||||
private SeriesTimerQuery seriesTimerQuery;
|
||||
|
||||
private ArtistsQuery artistsQuery;
|
||||
private AlbumArtistsQuery albumArtistsQuery;
|
||||
private SeasonQuery seasonQuery;
|
||||
private GetResumeItemsRequest resumeQuery;
|
||||
private GetSpecialsRequest specialsQuery;
|
||||
private QueryType queryType;
|
||||
|
||||
private int chunkSize = 0;
|
||||
@@ -39,11 +45,14 @@ public class BrowseRowDef {
|
||||
|
||||
private ChangeTriggerType[] changeTriggers;
|
||||
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize) {
|
||||
this(header, query, chunkSize, false, false);
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize) {
|
||||
this(header, query, chunkSize, false);
|
||||
}
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb) {
|
||||
this(header, query, chunkSize, preferParentThumb, false);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight) {
|
||||
headerText = header;
|
||||
this.query = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -52,15 +61,15 @@ public class BrowseRowDef {
|
||||
this.queryType = QueryType.Items;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
this(header, query, chunkSize, false, false, changeTriggers);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers) {
|
||||
this(header,query,chunkSize,preferParentThumb,staticHeight,changeTriggers,QueryType.Items);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetItemsRequest query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
|
||||
public BrowseRowDef(String header, ItemQuery query, int chunkSize, boolean preferParentThumb, boolean staticHeight, ChangeTriggerType[] changeTriggers, QueryType queryType) {
|
||||
headerText = header;
|
||||
this.query = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -70,7 +79,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, ArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.artistsQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -78,7 +87,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetAlbumArtistsRequest query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, AlbumArtistsQuery query, int chunkSize, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.albumArtistsQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
@@ -86,14 +95,20 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetSeriesTimersRequest query) {
|
||||
public BrowseRowDef(String header, NextUpQuery query) {
|
||||
headerText = header;
|
||||
this.nextUpQuery = query;
|
||||
this.queryType = QueryType.NextUp;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SeriesTimerQuery query) {
|
||||
headerText = header;
|
||||
this.seriesTimerQuery = query;
|
||||
this.staticHeight = true;
|
||||
this.queryType = QueryType.SeriesTimer;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetNextUpRequest query, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, NextUpQuery query, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.nextUpQuery = query;
|
||||
this.queryType = QueryType.NextUp;
|
||||
@@ -101,7 +116,7 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetLatestMediaRequest query, ChangeTriggerType[] changeTriggers) {
|
||||
public BrowseRowDef(String header, LatestItemsQuery query, ChangeTriggerType[] changeTriggers) {
|
||||
headerText = header;
|
||||
this.latestItemsQuery = query;
|
||||
this.queryType = QueryType.LatestItems;
|
||||
@@ -109,36 +124,62 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetLiveTvChannelsRequest query) {
|
||||
public BrowseRowDef(String header, SimilarItemsQuery query) {
|
||||
headerText = header;
|
||||
this.similarQuery = query;
|
||||
this.queryType = QueryType.SimilarSeries;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, LiveTvChannelQuery query) {
|
||||
headerText = header;
|
||||
this.tvChannelQuery = query;
|
||||
this.queryType = QueryType.LiveTvChannel;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetRecommendedProgramsRequest query) {
|
||||
public BrowseRowDef(String header, RecommendedProgramQuery query) {
|
||||
headerText = header;
|
||||
this.programQuery = query;
|
||||
this.queryType = QueryType.LiveTvProgram;
|
||||
this.changeTriggers = new ChangeTriggerType[] {ChangeTriggerType.GuideNeedsLoad};
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetRecordingsRequest query) {
|
||||
public BrowseRowDef(String header, RecordingQuery query) {
|
||||
this(header, query, 0);
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetRecordingsRequest query, int chunkSize) {
|
||||
public BrowseRowDef(String header, RecordingQuery query, int chunkSize) {
|
||||
headerText = header;
|
||||
this.recordingQuery = query;
|
||||
this.chunkSize = chunkSize;
|
||||
this.queryType = QueryType.LiveTvRecording;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetSimilarItemsRequest query, QueryType type) {
|
||||
public BrowseRowDef(String header, PersonsQuery query, int chunkSize) {
|
||||
headerText = header;
|
||||
this.personsQuery = query;
|
||||
this.queryType = QueryType.Persons;
|
||||
this.chunkSize = chunkSize;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, SimilarItemsQuery query, QueryType type) {
|
||||
headerText = header;
|
||||
this.similarQuery = query;
|
||||
this.queryType = type;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetUserViewsRequest query) {
|
||||
public BrowseRowDef(String header, SeasonQuery query) {
|
||||
headerText = header;
|
||||
this.seasonQuery = query;
|
||||
this.queryType = QueryType.Season;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, UpcomingEpisodesQuery query) {
|
||||
headerText = header;
|
||||
this.upcomingQuery = query;
|
||||
this.queryType = QueryType.Upcoming;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, ViewQuery query) {
|
||||
headerText = header;
|
||||
this.staticHeight = true;
|
||||
this.queryType = QueryType.Views;
|
||||
@@ -154,12 +195,6 @@ public class BrowseRowDef {
|
||||
this.changeTriggers = changeTriggers;
|
||||
}
|
||||
|
||||
public BrowseRowDef(String header, GetSpecialsRequest query) {
|
||||
headerText = header;
|
||||
this.specialsQuery = query;
|
||||
this.queryType = QueryType.Specials;
|
||||
}
|
||||
|
||||
public int getChunkSize() {
|
||||
return chunkSize;
|
||||
}
|
||||
@@ -170,43 +205,55 @@ public class BrowseRowDef {
|
||||
return headerText;
|
||||
}
|
||||
|
||||
public GetItemsRequest getQuery() {
|
||||
public void setHeaderText(String headerText) {
|
||||
this.headerText = headerText;
|
||||
}
|
||||
|
||||
public ItemQuery getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public GetNextUpRequest getNextUpQuery() {
|
||||
public NextUpQuery getNextUpQuery() {
|
||||
return nextUpQuery;
|
||||
}
|
||||
|
||||
public GetLatestMediaRequest getLatestItemsQuery() { return latestItemsQuery; }
|
||||
public LatestItemsQuery getLatestItemsQuery() { return latestItemsQuery; }
|
||||
|
||||
public GetSimilarItemsRequest getSimilarQuery() { return similarQuery; }
|
||||
public SimilarItemsQuery getSimilarQuery() { return similarQuery; }
|
||||
|
||||
public QueryType getQueryType() {
|
||||
return queryType;
|
||||
}
|
||||
|
||||
public GetLiveTvChannelsRequest getTvChannelQuery() {
|
||||
public SeasonQuery getSeasonQuery() { return seasonQuery; }
|
||||
|
||||
public UpcomingEpisodesQuery getUpcomingQuery() {
|
||||
return upcomingQuery;
|
||||
}
|
||||
|
||||
public LiveTvChannelQuery getTvChannelQuery() {
|
||||
return tvChannelQuery;
|
||||
}
|
||||
|
||||
public GetRecommendedProgramsRequest getProgramQuery() {
|
||||
public RecommendedProgramQuery getProgramQuery() {
|
||||
return programQuery;
|
||||
}
|
||||
|
||||
public GetRecordingsRequest getRecordingQuery() { return recordingQuery; }
|
||||
public RecordingQuery getRecordingQuery() { return recordingQuery; }
|
||||
|
||||
public boolean getPreferParentThumb() { return preferParentThumb; }
|
||||
|
||||
public GetArtistsRequest getArtistsQuery() { return artistsQuery; }
|
||||
public GetAlbumArtistsRequest getAlbumArtistsQuery() { return albumArtistsQuery; }
|
||||
public PersonsQuery getPersonsQuery() {
|
||||
return personsQuery;
|
||||
}
|
||||
|
||||
public GetSeriesTimersRequest getSeriesTimerQuery() { return seriesTimerQuery; }
|
||||
public ArtistsQuery getArtistsQuery() { return artistsQuery; }
|
||||
public AlbumArtistsQuery getAlbumArtistsQuery() { return albumArtistsQuery; }
|
||||
|
||||
public SeriesTimerQuery getSeriesTimerQuery() { return seriesTimerQuery; }
|
||||
|
||||
public GetResumeItemsRequest getResumeQuery() { return resumeQuery; }
|
||||
|
||||
public GetSpecialsRequest getSpecialsQuery() { return specialsQuery; }
|
||||
|
||||
public ChangeTriggerType[] getChangeTriggers() {
|
||||
return changeTriggers;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.ui.livetv.TvManager;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
|
||||
public class BrowseScheduleFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@@ -12,6 +14,7 @@ public class BrowseScheduleFragment extends EnhancedBrowseFragment {
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
TvManager.getScheduleRowsAsync(this, null, new CardPresenter(true), mRowsAdapter);
|
||||
TvManager.getScheduleRowsAsync(requireContext(), new TimerQuery(), new CardPresenter(true), mRowsAdapter, new LifecycleAwareResponse<Integer>(getLifecycle()) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import androidx.leanback.widget.ArrayObjectAdapter;
|
||||
import androidx.leanback.widget.HeaderItem;
|
||||
import androidx.leanback.widget.ListRow;
|
||||
@@ -8,185 +10,478 @@ import androidx.leanback.widget.Row;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.auth.repository.UserRepository;
|
||||
import org.jellyfin.androidtv.constant.ChangeTriggerType;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.LiveTvOption;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.querying.GetSeriesTimersRequest;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.preference.UserPreferences;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.presentation.GridButtonPresenter;
|
||||
import org.jellyfin.androidtv.ui.presentation.MutableObjectAdapter;
|
||||
import org.jellyfin.androidtv.util.TimeUtils;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.api.CollectionType;
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto;
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemDto;
|
||||
import org.jellyfin.apiclient.model.dto.BaseItemType;
|
||||
import org.jellyfin.apiclient.model.entities.LocationType;
|
||||
import org.jellyfin.apiclient.model.entities.SortOrder;
|
||||
import org.jellyfin.apiclient.model.livetv.LiveTvChannelQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecommendedProgramQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.RecordingQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.SeriesTimerQuery;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerInfoDto;
|
||||
import org.jellyfin.apiclient.model.livetv.TimerQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter;
|
||||
import org.jellyfin.apiclient.model.querying.ItemQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemsResult;
|
||||
import org.jellyfin.apiclient.model.querying.LatestItemsQuery;
|
||||
import org.jellyfin.apiclient.model.querying.NextUpQuery;
|
||||
import org.jellyfin.apiclient.model.results.TimerInfoDtoResult;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import kotlin.Lazy;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BrowseViewFragment extends EnhancedBrowseFragment {
|
||||
private boolean isLiveTvLibrary;
|
||||
|
||||
private Lazy<ApiClient> apiClient = inject(ApiClient.class);
|
||||
|
||||
@Override
|
||||
protected void setupQueries(final RowLoader rowLoader) {
|
||||
CollectionType type = mFolder != null && mFolder.getCollectionType() != null ? mFolder.getCollectionType() : CollectionType.UNKNOWN;
|
||||
String type = mFolder.getCollectionType() != null ? mFolder.getCollectionType().toLowerCase() : "";
|
||||
switch (type) {
|
||||
case MOVIES:
|
||||
itemType = BaseItemKind.MOVIE;
|
||||
case CollectionType.Movies:
|
||||
itemTypeString = "Movie";
|
||||
|
||||
//Resume
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
|
||||
StdItemQuery resumeMovies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
resumeMovies.setIncludeItemTypes(new String[]{"Movie"});
|
||||
resumeMovies.setRecursive(true);
|
||||
resumeMovies.setParentId(mFolder.getId().toString());
|
||||
resumeMovies.setImageTypeLimit(1);
|
||||
resumeMovies.setLimit(50);
|
||||
resumeMovies.setCollapseBoxSetItems(false);
|
||||
resumeMovies.setEnableTotalRecordCount(false);
|
||||
resumeMovies.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resumeMovies.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resumeMovies.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeMovies, 0, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback}));
|
||||
|
||||
//Latest
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId()), new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
LatestItemsQuery latestMovies = new LatestItemsQuery();
|
||||
latestMovies.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
latestMovies.setParentId(mFolder.getId().toString());
|
||||
latestMovies.setLimit(50);
|
||||
latestMovies.setImageTypeLimit(1);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestMovies, new ChangeTriggerType[]{ChangeTriggerType.MoviePlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MOVIE), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
StdItemQuery favorites = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
favorites.setIncludeItemTypes(new String[]{"Movie"});
|
||||
favorites.setRecursive(true);
|
||||
favorites.setParentId(mFolder.getId().toString());
|
||||
favorites.setImageTypeLimit(1);
|
||||
favorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
favorites.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
//Collections
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), BrowsingUtils.createCollectionsRequest(mFolder.getId()), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
StdItemQuery collections = new StdItemQuery();
|
||||
collections.setFields(new ItemFields[]{
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
collections.setIncludeItemTypes(new String[]{"BoxSet"});
|
||||
collections.setRecursive(true);
|
||||
collections.setImageTypeLimit(1);
|
||||
//collections.setParentId(mFolder.getId());
|
||||
collections.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_collections), collections, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case TVSHOWS:
|
||||
itemType = BaseItemKind.SERIES;
|
||||
case CollectionType.TvShows:
|
||||
itemTypeString = "Series";
|
||||
|
||||
//Resume
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), BrowsingUtils.createResumeItemsRequest(mFolder.getId(), BaseItemKind.EPISODE), 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
StdItemQuery resumeEpisodes = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
resumeEpisodes.setIncludeItemTypes(new String[]{"Episode"});
|
||||
resumeEpisodes.setLimit(50);
|
||||
resumeEpisodes.setParentId(mFolder.getId().toString());
|
||||
resumeEpisodes.setRecursive(true);
|
||||
resumeEpisodes.setImageTypeLimit(1);
|
||||
resumeEpisodes.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resumeEpisodes.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resumeEpisodes.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resumeEpisodes, 0, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
|
||||
//Next up
|
||||
GetNextUpRequest getNextUpRequest = BrowsingUtils.createGetNextUpRequest(mFolder.getId());
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), getNextUpRequest, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
NextUpQuery nextUpQuery = new NextUpQuery();
|
||||
nextUpQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
nextUpQuery.setLimit(50);
|
||||
nextUpQuery.setParentId(mFolder.getId().toString());
|
||||
nextUpQuery.setImageTypeLimit(1);
|
||||
nextUpQuery.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_next_up), nextUpQuery, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}));
|
||||
|
||||
//Premieres
|
||||
if (KoinJavaComponent.<UserPreferences>get(UserPreferences.class).get(UserPreferences.Companion.getPremieresEnabled())) {
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), BrowsingUtils.createPremieresRequest(mFolder.getId()), 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
StdItemQuery newQuery = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.DateCreated,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
newQuery.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
newQuery.setIncludeItemTypes(new String[]{"Episode"});
|
||||
newQuery.setParentId(mFolder.getId().toString());
|
||||
newQuery.setRecursive(true);
|
||||
newQuery.setIsVirtualUnaired(false);
|
||||
newQuery.setIsMissing(false);
|
||||
newQuery.setImageTypeLimit(1);
|
||||
newQuery.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
|
||||
newQuery.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
newQuery.setSortOrder(SortOrder.Descending);
|
||||
newQuery.setEnableTotalRecordCount(false);
|
||||
newQuery.setLimit(300);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_new_premieres), newQuery, 0, true, true, new ChangeTriggerType[]{ChangeTriggerType.TvPlayback}, QueryType.Premieres));
|
||||
}
|
||||
|
||||
//Latest content added
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.EPISODE, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
LatestItemsQuery latestSeries = new LatestItemsQuery();
|
||||
latestSeries.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaStreams
|
||||
});
|
||||
latestSeries.setIncludeItemTypes(new String[]{"Episode"});
|
||||
latestSeries.setGroupItems(true);
|
||||
latestSeries.setParentId(mFolder.getId().toString());
|
||||
latestSeries.setLimit(50);
|
||||
latestSeries.setImageTypeLimit(1);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestSeries, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.SERIES), 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
StdItemQuery tvFavorites = new StdItemQuery();
|
||||
tvFavorites.setIncludeItemTypes(new String[]{"Series"});
|
||||
tvFavorites.setRecursive(true);
|
||||
tvFavorites.setParentId(mFolder.getId().toString());
|
||||
tvFavorites.setImageTypeLimit(1);
|
||||
tvFavorites.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
tvFavorites.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), tvFavorites, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case MUSIC:
|
||||
case CollectionType.Music:
|
||||
//Latest
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), BrowsingUtils.createLatestMediaRequest(mFolder.getId(), BaseItemKind.AUDIO, true), new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
LatestItemsQuery latestAlbums = new LatestItemsQuery();
|
||||
latestAlbums.setFields(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
latestAlbums.setIncludeItemTypes(new String[]{"Audio"});
|
||||
latestAlbums.setGroupItems(true);
|
||||
latestAlbums.setImageTypeLimit(1);
|
||||
latestAlbums.setParentId(mFolder.getId().toString());
|
||||
latestAlbums.setLimit(50);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latestAlbums, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Last Played
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), BrowsingUtils.createLastPlayedRequest(mFolder.getId()), 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
StdItemQuery lastPlayed = new StdItemQuery();
|
||||
lastPlayed.setIncludeItemTypes(new String[]{"Audio"});
|
||||
lastPlayed.setRecursive(true);
|
||||
lastPlayed.setParentId(mFolder.getId().toString());
|
||||
lastPlayed.setImageTypeLimit(1);
|
||||
lastPlayed.setFilters(new ItemFilter[]{ItemFilter.IsPlayed});
|
||||
lastPlayed.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
lastPlayed.setSortOrder(SortOrder.Descending);
|
||||
lastPlayed.setEnableTotalRecordCount(false);
|
||||
lastPlayed.setLimit(50);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_last_played), lastPlayed, 0, false, true, new ChangeTriggerType[]{ChangeTriggerType.MusicPlayback, ChangeTriggerType.LibraryUpdated}));
|
||||
|
||||
//Favorites
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), BrowsingUtils.createFavoriteItemsRequest(mFolder.getId(), BaseItemKind.MUSIC_ALBUM), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
StdItemQuery favAlbums = new StdItemQuery();
|
||||
favAlbums.setIncludeItemTypes(new String[]{"MusicAlbum", "MusicArtist"});
|
||||
favAlbums.setRecursive(true);
|
||||
favAlbums.setParentId(mFolder.getId().toString());
|
||||
favAlbums.setImageTypeLimit(1);
|
||||
favAlbums.setFilters(new ItemFilter[]{ItemFilter.IsFavorite});
|
||||
favAlbums.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorites), favAlbums, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated, ChangeTriggerType.FavoriteUpdate}));
|
||||
|
||||
//AudioPlaylists
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), BrowsingUtils.createPlaylistsRequest(), 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
|
||||
StdItemQuery playlists = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.CumulativeRunTimeTicks,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
playlists.setIncludeItemTypes(new String[]{"Playlist"});
|
||||
playlists.setImageTypeLimit(1);
|
||||
playlists.setRecursive(true);
|
||||
playlists.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
playlists.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_playlists), playlists, 60, false, true, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}, QueryType.AudioPlaylists));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
case LIVETV:
|
||||
case CollectionType.LiveTv:
|
||||
isLiveTvLibrary = true;
|
||||
showViews = true;
|
||||
|
||||
//On now
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), BrowsingUtils.createLiveTVOnNowRequest()));
|
||||
RecommendedProgramQuery onNow = new RecommendedProgramQuery();
|
||||
onNow.setIsAiring(true);
|
||||
onNow.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
onNow.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
onNow.setImageTypeLimit(1);
|
||||
onNow.setEnableTotalRecordCount(false);
|
||||
onNow.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_on_now), onNow));
|
||||
|
||||
//Upcoming
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), BrowsingUtils.createLiveTVUpcomingRequest()));
|
||||
RecommendedProgramQuery upcomingTv = new RecommendedProgramQuery();
|
||||
upcomingTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
upcomingTv.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChannelInfo,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
upcomingTv.setIsAiring(false);
|
||||
upcomingTv.setHasAired(false);
|
||||
upcomingTv.setImageTypeLimit(1);
|
||||
upcomingTv.setEnableTotalRecordCount(false);
|
||||
upcomingTv.setLimit(150);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_coming_up), upcomingTv));
|
||||
|
||||
//Fav Channels
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), BrowsingUtils.createLiveTVChannelsRequest(true)));
|
||||
LiveTvChannelQuery favTv = new LiveTvChannelQuery();
|
||||
favTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
favTv.setEnableFavoriteSorting(true);
|
||||
favTv.setIsFavorite(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_favorite_channels), favTv));
|
||||
|
||||
//Other Channels
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), BrowsingUtils.createLiveTVChannelsRequest(false)));
|
||||
LiveTvChannelQuery otherTv = new LiveTvChannelQuery();
|
||||
otherTv.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
otherTv.setIsFavorite(false);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other_channels), otherTv));
|
||||
|
||||
//Latest Recordings
|
||||
BrowseViewFragmentHelperKt.getLiveTvRecordingsAndTimers(this, (recordings, timers) -> {
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
LocalDateTime next24 = LocalDateTime.now().plusDays(1);
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : timers.getItems()) {
|
||||
if (timer.getStartDate().isBefore(next24)) {
|
||||
nearTimers.add(BrowseViewFragmentHelperKt.getTimerProgramInfo(timer));
|
||||
}
|
||||
}
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
recordings.setLimit(40);
|
||||
|
||||
if (recordings.getTotalRecordCount() > 0) {
|
||||
List<BaseItemDto> dayItems = new ArrayList<>();
|
||||
List<BaseItemDto> weekItems = new ArrayList<>();
|
||||
//Do a straight query and then split the returned items into logical groups
|
||||
apiClient.getValue().GetLiveTvRecordingsAsync(recordings, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
LocalDateTime past24 = LocalDateTime.now().minusDays(1);
|
||||
LocalDateTime pastWeek = LocalDateTime.now().minusWeeks(1);
|
||||
for (BaseItemDto item : recordings.getItems()) {
|
||||
if (item.getDateCreated() != null) {
|
||||
if (item.getDateCreated().isAfter(past24)) {
|
||||
dayItems.add(item);
|
||||
} else if (item.getDateCreated().isAfter(pastWeek)) {
|
||||
weekItems.add(item);
|
||||
final ItemsResult recordingsResponse = response;
|
||||
final long ticks24 = 1000 * 60 * 60 * 24;
|
||||
|
||||
// Also get scheduled recordings for next 24 hours
|
||||
final TimerQuery scheduled = new TimerQuery();
|
||||
apiClient.getValue().GetLiveTvTimersAsync(scheduled, new LifecycleAwareResponse<TimerInfoDtoResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(TimerInfoDtoResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
List<BaseItemDto> nearTimers = new ArrayList<>();
|
||||
long next24 = System.currentTimeMillis() + ticks24;
|
||||
//Get scheduled items for next 24 hours
|
||||
for (TimerInfoDto timer : response.getItems()) {
|
||||
if (TimeUtils.convertToLocalDate(timer.getStartDate()).getTime() <= next24) {
|
||||
BaseItemDto programInfo = timer.getProgramInfo();
|
||||
if (programInfo == null) {
|
||||
programInfo = new BaseItemDto();
|
||||
programInfo.setId(timer.getId());
|
||||
programInfo.setChannelName(timer.getChannelName());
|
||||
programInfo.setName(Utils.getSafeValue(timer.getName(), "Unknown"));
|
||||
Timber.w("No program info for timer %s. Creating one...", programInfo.getName());
|
||||
programInfo.setBaseItemType(BaseItemType.Program);
|
||||
programInfo.setTimerId(timer.getId());
|
||||
programInfo.setSeriesTimerId(timer.getSeriesTimerId());
|
||||
programInfo.setStartDate(timer.getStartDate());
|
||||
programInfo.setEndDate(timer.getEndDate());
|
||||
}
|
||||
programInfo.setLocationType(LocationType.Virtual);
|
||||
nearTimers.add(programInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (recordingsResponse.getTotalRecordCount() > 0) {
|
||||
List<BaseItemDto> dayItems = new ArrayList<>();
|
||||
List<BaseItemDto> weekItems = new ArrayList<>();
|
||||
|
||||
long past24 = System.currentTimeMillis() - ticks24;
|
||||
long pastWeek = System.currentTimeMillis() - (ticks24 * 7);
|
||||
for (BaseItemDto item : recordingsResponse.getItems()) {
|
||||
if (item.getDateCreated() != null) {
|
||||
if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= past24) {
|
||||
dayItems.add(item);
|
||||
} else if (TimeUtils.convertToLocalDate(item.getDateCreated()).getTime() >= pastWeek) {
|
||||
weekItems.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//First put all recordings in and retrieve
|
||||
//All Recordings
|
||||
RecordingQuery recordings = new RecordingQuery();
|
||||
recordings.setFields(new ItemFields[]{
|
||||
ItemFields.Overview,
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.ChildCount
|
||||
});
|
||||
recordings.setUserId(KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString());
|
||||
recordings.setEnableImages(true);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), recordings, 50));
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
//Now insert our smart rows
|
||||
if (weekItems.size() > 0) {
|
||||
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
|
||||
weekAdapter.Retrieve();
|
||||
ListRow weekRow = new ListRow(new HeaderItem("Past Week"), weekAdapter);
|
||||
mRowsAdapter.add(0, weekRow);
|
||||
}
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
}
|
||||
if (dayItems.size() > 0) {
|
||||
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
|
||||
dayAdapter.Retrieve();
|
||||
ListRow dayRow = new ListRow(new HeaderItem("Past 24 Hours"), dayAdapter);
|
||||
mRowsAdapter.add(0, dayRow);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no recordings
|
||||
rowLoader.loadRows(mRows);
|
||||
if (nearTimers.size() > 0) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem("Scheduled in Next 24 Hours"), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
} else {
|
||||
mTitle.setText(R.string.lbl_no_recordings);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//First put all recordings in and retrieve
|
||||
//All Recordings
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_recent_recordings), BrowsingUtils.createLiveTVRecordingsRequest(), 50));
|
||||
rowLoader.loadRows(mRows);
|
||||
|
||||
//Now insert our smart rows
|
||||
if (!weekItems.isEmpty()) {
|
||||
ItemRowAdapter weekAdapter = new ItemRowAdapter(requireContext(), weekItems, mCardPresenter, mRowsAdapter, true);
|
||||
weekAdapter.Retrieve();
|
||||
ListRow weekRow = new ListRow(new HeaderItem(getString(R.string.past_week)), weekAdapter);
|
||||
mRowsAdapter.add(0, weekRow);
|
||||
}
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
}
|
||||
if (!dayItems.isEmpty()) {
|
||||
ItemRowAdapter dayAdapter = new ItemRowAdapter(requireContext(), dayItems, mCardPresenter, mRowsAdapter, true);
|
||||
dayAdapter.Retrieve();
|
||||
ListRow dayRow = new ListRow(new HeaderItem(getString(R.string.past_24_hours)), dayAdapter);
|
||||
mRowsAdapter.add(0, dayRow);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no recordings
|
||||
rowLoader.loadRows(mRows);
|
||||
if (!nearTimers.isEmpty()) {
|
||||
ItemRowAdapter scheduledAdapter = new ItemRowAdapter(requireContext(), nearTimers, mCardPresenter, mRowsAdapter, true);
|
||||
scheduledAdapter.Retrieve();
|
||||
ListRow scheduleRow = new ListRow(new HeaderItem(getString(R.string.scheduled_in_next_24_hours)), scheduledAdapter);
|
||||
mRowsAdapter.add(0, scheduleRow);
|
||||
} else {
|
||||
mTitle.setText(R.string.lbl_no_recordings);
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}, exception -> {
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
return null;
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
if (!getActive()) return;
|
||||
|
||||
Utils.showToast(getContext(), exception.getLocalizedMessage());
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "seriestimers":
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), new SeriesTimerQuery()));
|
||||
rowLoader.loadRows(mRows);
|
||||
break;
|
||||
default:
|
||||
boolean isRecordingsView = getArguments().getBoolean(Extras.IsLiveTvSeriesRecordings, false);
|
||||
if (isRecordingsView) {
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_series_recordings), GetSeriesTimersRequest.INSTANCE));
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
// Fall back to rows defined by the view children
|
||||
final List<BrowseRowDef> rows = new ArrayList<>();
|
||||
final UUID userId = KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId();
|
||||
|
||||
ItemQuery query = new ItemQuery();
|
||||
query.setParentId(mFolder.getId().toString());
|
||||
query.setUserId(userId.toString());
|
||||
query.setImageTypeLimit(1);
|
||||
query.setSortBy(new String[]{ItemSortBy.SortName});
|
||||
|
||||
apiClient.getValue().GetItemsAsync(query, new LifecycleAwareResponse<ItemsResult>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(ItemsResult response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
if (response.getTotalRecordCount() > 0) {
|
||||
for (BaseItemDto item : response.getItems()) {
|
||||
ItemQuery rowQuery = new StdItemQuery();
|
||||
rowQuery.setParentId(item.getId());
|
||||
rowQuery.setUserId(userId.toString());
|
||||
rows.add(new BrowseRowDef(item.getName(), rowQuery, 60, new ChangeTriggerType[]{ChangeTriggerType.LibraryUpdated}));
|
||||
}
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
Timber.e(exception, "Failed to get items");
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.LocationType
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||
import org.jellyfin.sdk.model.api.TimerInfoDtoQueryResult
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
fun EnhancedBrowseFragment.getLiveTvRecordingsAndTimers(
|
||||
callback: (recordings: BaseItemDtoQueryResult, timers: TimerInfoDtoQueryResult) -> Unit,
|
||||
errorCallback: (exception: Throwable) -> Unit,
|
||||
) {
|
||||
val api by inject<ApiClient>()
|
||||
|
||||
lifecycleScope.launch {
|
||||
runCatching {
|
||||
val recordings by api.liveTvApi.getRecordings(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 40,
|
||||
)
|
||||
|
||||
val timers by api.liveTvApi.getTimers()
|
||||
|
||||
recordings to timers
|
||||
}.fold(
|
||||
onSuccess = { (recordings, timers) -> callback(recordings, timers) },
|
||||
onFailure = { exception -> errorCallback(exception) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun EnhancedBrowseFragment.getLiveTvTimers(
|
||||
callback: (timers: TimerInfoDtoQueryResult) -> Unit,
|
||||
errorCallback: (exception: Throwable) -> Unit,
|
||||
) {
|
||||
val api by inject<ApiClient>()
|
||||
|
||||
lifecycleScope.launch {
|
||||
runCatching {
|
||||
api.liveTvApi.getTimers().content
|
||||
}.fold(
|
||||
onSuccess = { timers -> callback(timers) },
|
||||
onFailure = { exception -> errorCallback(exception) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimerProgramInfo(timer: TimerInfoDto): BaseItemDto {
|
||||
val programInfo = timer.programInfo ?: BaseItemDto(
|
||||
id = requireNotNull(timer.id?.toUUIDOrNull()),
|
||||
channelName = timer.channelName,
|
||||
name = timer.name.orEmpty(),
|
||||
type = BaseItemKind.PROGRAM,
|
||||
timerId = timer.id,
|
||||
seriesTimerId = timer.seriesTimerId,
|
||||
startDate = timer.startDate,
|
||||
endDate = timer.endDate,
|
||||
mediaType = MediaType.UNKNOWN,
|
||||
)
|
||||
return programInfo.copy(
|
||||
locationType = LocationType.VIRTUAL,
|
||||
)
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetAlbumArtistsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetArtistsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSeasonsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetUpcomingEpisodesRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
object BrowsingUtils {
|
||||
@JvmStatic
|
||||
fun getRandomItem(
|
||||
api: ApiClient,
|
||||
lifecycle: LifecycleOwner,
|
||||
library: BaseItemDto,
|
||||
type: BaseItemKind,
|
||||
callback: (item: BaseItemDto?) -> Unit
|
||||
) {
|
||||
lifecycle.lifecycleScope.launch {
|
||||
try {
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = library.id,
|
||||
includeItemTypes = setOf(type),
|
||||
recursive = true,
|
||||
sortBy = setOf(ItemSortBy.RANDOM),
|
||||
limit = 1,
|
||||
)
|
||||
|
||||
callback(result.items?.firstOrNull())
|
||||
} catch (error: ApiClientException) {
|
||||
Timber.w(error, "Failed to retrieve random item")
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
|
||||
limit = 50,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
)
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSeriesGetNextUpRequest(parentId: UUID) = GetNextUpRequest(
|
||||
seriesId = parentId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
)
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createLatestMediaRequest(
|
||||
parentId: UUID,
|
||||
itemType: BaseItemKind? = null,
|
||||
groupItems: Boolean? = null
|
||||
) = GetLatestMediaRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
),
|
||||
parentId = parentId,
|
||||
limit = 50,
|
||||
imageTypeLimit = 1,
|
||||
includeItemTypes = itemType?.let(::setOf),
|
||||
groupItems = groupItems,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSeasonsRequest(seriesId: UUID) = GetSeasonsRequest(
|
||||
seriesId = seriesId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createUpcomingEpisodesRequest(parentId: UUID) = GetUpcomingEpisodesRequest(
|
||||
parentId = parentId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createSimilarItemsRequest(itemId: UUID) = GetSimilarItemsRequest(
|
||||
itemId = itemId,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
limit = 20,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVOnNowRequest() = GetRecommendedProgramsRequest(
|
||||
isAiring = true,
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHANNEL_INFO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
imageTypeLimit = 1,
|
||||
enableTotalRecordCount = false,
|
||||
limit = 150,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVUpcomingRequest() = GetRecommendedProgramsRequest(
|
||||
isAiring = false,
|
||||
hasAired = false,
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHANNEL_INFO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
imageTypeLimit = 1,
|
||||
enableTotalRecordCount = false,
|
||||
limit = 150,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createLiveTVRecordingsRequest(limit: Int? = null) = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVMovieRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isMovie = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVSeriesRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isSeries = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVSportsRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isSports = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVKidsRecordingsRequest() = GetRecordingsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
enableImages = true,
|
||||
limit = 60,
|
||||
isKids = true,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLiveTVChannelsRequest(isFavorite: Boolean) = GetLiveTvChannelsRequest(
|
||||
isFavorite = isFavorite,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createAlbumArtistsRequest(parentId: UUID) = GetAlbumArtistsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = parentId,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createArtistsRequest(parentId: UUID) = GetArtistsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = parentId,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPersonItemsRequest(personId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
personIds = setOf(personId),
|
||||
recursive = true,
|
||||
includeItemTypes = setOf(itemType),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createArtistItemsRequest(artistId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
artistIds = setOf(artistId),
|
||||
recursive = true,
|
||||
includeItemTypes = setOf(itemType),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createNextEpisodesRequest(seasonId: UUID, indexNumber: Int) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
parentId = seasonId,
|
||||
includeItemTypes = setOf(BaseItemKind.EPISODE),
|
||||
startIndex = indexNumber,
|
||||
limit = 20,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createResumeItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
),
|
||||
includeItemTypes = setOf(itemType),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
limit = 50,
|
||||
collapseBoxSetItems = false,
|
||||
enableTotalRecordCount = false,
|
||||
filters = setOf(ItemFilter.IS_RESUMABLE),
|
||||
sortBy = setOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createFavoriteItemsRequest(parentId: UUID, itemType: BaseItemKind) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
),
|
||||
includeItemTypes = setOf(itemType),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_FAVORITE),
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createCollectionsRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(ItemFields.CHILD_COUNT),
|
||||
includeItemTypes = setOf(BaseItemKind.BOX_SET),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
parentId = parentId,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPremieresRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.DATE_CREATED,
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.EPISODE),
|
||||
parentId = parentId,
|
||||
recursive = true,
|
||||
isMissing = false,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_UNPLAYED),
|
||||
sortBy = setOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
enableTotalRecordCount = false,
|
||||
limit = 300,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLastPlayedRequest(parentId: UUID) = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.AUDIO),
|
||||
recursive = true,
|
||||
parentId = parentId,
|
||||
imageTypeLimit = 1,
|
||||
filters = setOf(ItemFilter.IS_PLAYED),
|
||||
sortBy = setOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
enableTotalRecordCount = false,
|
||||
limit = 50,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createPlaylistsRequest() = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CUMULATIVE_RUN_TIME_TICKS,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
includeItemTypes = setOf(BaseItemKind.PLAYLIST),
|
||||
imageTypeLimit = 1,
|
||||
recursive = true,
|
||||
sortBy = setOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createBrowseGridItemsRequest(parent: BaseItemDto): GetItemsRequest {
|
||||
val baseRequest = GetItemsRequest(
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
),
|
||||
parentId = parent.id,
|
||||
)
|
||||
|
||||
if (parent.type == BaseItemKind.USER_VIEW || parent.type == BaseItemKind.COLLECTION_FOLDER) {
|
||||
return when (parent.collectionType) {
|
||||
CollectionType.MOVIES -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.MOVIE),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.TVSHOWS -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.SERIES),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.BOXSETS -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.BOX_SET),
|
||||
parentId = null,
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
CollectionType.MUSIC -> baseRequest.copy(
|
||||
includeItemTypes = setOf(BaseItemKind.MUSIC_ALBUM),
|
||||
recursive = true,
|
||||
)
|
||||
|
||||
else -> baseRequest
|
||||
}
|
||||
}
|
||||
|
||||
return baseRequest
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class ByGenreFragment : BrowseFolderFragment() {
|
||||
@@ -18,25 +16,19 @@ class ByGenreFragment : BrowseFolderFragment() {
|
||||
// Get all genres for this folder
|
||||
val genresResponse by apiClient.genresApi.getGenres(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
sortBy = setOf(ItemSortBy.SortName),
|
||||
userId = apiClient.userId,
|
||||
)
|
||||
|
||||
for (genre in genresResponse.items.orEmpty()) {
|
||||
val itemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
genres = setOf(genre.name.orEmpty()),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
rows.add(BrowseRowDef(genre.name, itemsRequest, 40))
|
||||
val itemsQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
genres = arrayOf(genre.name)
|
||||
recursive = true
|
||||
}
|
||||
rows.add(BrowseRowDef(genre.name, itemsQuery, 40))
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy
|
||||
|
||||
class ByLetterFragment : BrowseFolderFragment() {
|
||||
override suspend fun setupQueries(rowLoader: RowLoader) {
|
||||
@@ -14,41 +12,27 @@ class ByLetterFragment : BrowseFolderFragment() {
|
||||
val letters = getString(R.string.byletter_letters)
|
||||
|
||||
// Add a '#' item
|
||||
val numbersItemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
nameLessThan = letters.substring(0, 1),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
val numbersQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id?.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
nameLessThan = letters.substring(0, 1)
|
||||
recursive = true
|
||||
}
|
||||
|
||||
rows.add(BrowseRowDef("#", numbersItemsRequest, 40))
|
||||
rows.add(BrowseRowDef("#", numbersQuery, 40))
|
||||
|
||||
// Add all the defined letters
|
||||
for (letter in letters.toCharArray()) {
|
||||
val letterItemsRequest = GetItemsRequest(
|
||||
parentId = folder?.id,
|
||||
sortBy = setOf(ItemSortBy.SORT_NAME),
|
||||
includeItemTypes = includeType?.let(BaseItemKind::fromNameOrNull)?.let(::setOf),
|
||||
nameStartsWith = letter.toString(),
|
||||
recursive = true,
|
||||
fields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
),
|
||||
)
|
||||
val letterQuery = StdItemQuery().apply {
|
||||
parentId = folder?.id?.toString()
|
||||
sortBy = arrayOf(ItemSortBy.SortName)
|
||||
includeType?.let { includeItemTypes = arrayOf(it) }
|
||||
nameStartsWith = letter.toString()
|
||||
recursive = true
|
||||
}
|
||||
|
||||
rows.add(BrowseRowDef(letter.toString(), letterItemsRequest, 40))
|
||||
rows.add(BrowseRowDef(letter.toString(), letterQuery, 40))
|
||||
}
|
||||
|
||||
rowLoader.loadRows(rows)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFields;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
|
||||
public class CollectionFragment extends EnhancedBrowseFragment {
|
||||
@Override
|
||||
protected void setupQueries(RowLoader rowLoader) {
|
||||
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
movies.setParentId(mFolder.getId().toString());
|
||||
movies.setIncludeItemTypes(new String[]{BaseItemKind.MOVIE.getSerialName()});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_movies), movies, 100));
|
||||
|
||||
StdItemQuery series = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
series.setParentId(mFolder.getId().toString());
|
||||
series.setIncludeItemTypes(new String[]{BaseItemKind.SERIES.getSerialName()});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_tv_series), series, 100));
|
||||
|
||||
StdItemQuery others = new StdItemQuery(new ItemFields[]{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ItemCounts,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.ChildCount,
|
||||
ItemFields.MediaStreams,
|
||||
ItemFields.MediaSources
|
||||
});
|
||||
others.setParentId(mFolder.getId().toString());
|
||||
others.setExcludeItemTypes(new String[]{BaseItemKind.MOVIE.getSerialName(), BaseItemKind.SERIES.getSerialName()});
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_other), others, 100));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
|
||||
class CollectionFragment : EnhancedBrowseFragment() {
|
||||
companion object {
|
||||
private val itemFields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.MEDIA_STREAMS,
|
||||
ItemFields.MEDIA_SOURCES,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setupQueries(rowLoader: RowLoader) {
|
||||
val movies = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
includeItemTypes = setOf(BaseItemKind.MOVIE),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_movies), movies, 100))
|
||||
|
||||
val series = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
includeItemTypes = setOf(BaseItemKind.SERIES),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_tv_series), series, 100))
|
||||
|
||||
val others = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
excludeItemTypes = setOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_other), others, 100))
|
||||
|
||||
rowLoader.loadRows(mRows)
|
||||
}
|
||||
}
|
||||
@@ -30,20 +30,17 @@ import androidx.lifecycle.Lifecycle;
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.constant.CustomMessage;
|
||||
import org.jellyfin.androidtv.constant.Extras;
|
||||
import org.jellyfin.androidtv.constant.ImageType;
|
||||
import org.jellyfin.androidtv.constant.LiveTvOption;
|
||||
import org.jellyfin.androidtv.constant.QueryType;
|
||||
import org.jellyfin.androidtv.data.model.DataRefreshService;
|
||||
import org.jellyfin.androidtv.data.querying.GetUserViewsRequest;
|
||||
import org.jellyfin.androidtv.data.querying.ViewQuery;
|
||||
import org.jellyfin.androidtv.data.repository.CustomMessageRepository;
|
||||
import org.jellyfin.androidtv.data.service.BackgroundService;
|
||||
import org.jellyfin.androidtv.databinding.EnhancedDetailBrowseBinding;
|
||||
import org.jellyfin.androidtv.ui.GridButton;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.BaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.GridButtonBaseRowItem;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemLauncher;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapter;
|
||||
import org.jellyfin.androidtv.ui.itemhandling.ItemRowAdapterHelperKt;
|
||||
import org.jellyfin.androidtv.ui.navigation.Destinations;
|
||||
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
|
||||
import org.jellyfin.androidtv.ui.presentation.CardPresenter;
|
||||
@@ -54,10 +51,12 @@ import org.jellyfin.androidtv.util.CoroutineUtils;
|
||||
import org.jellyfin.androidtv.util.InfoLayoutHelper;
|
||||
import org.jellyfin.androidtv.util.KeyProcessor;
|
||||
import org.jellyfin.androidtv.util.MarkdownRenderer;
|
||||
import org.jellyfin.androidtv.util.apiclient.LifecycleAwareResponse;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.FakeBaseItem;
|
||||
import org.jellyfin.androidtv.util.sdk.compat.JavaCompat;
|
||||
import org.jellyfin.sdk.api.client.ApiClient;
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.constant.CollectionType;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -73,7 +72,6 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
protected static final int BY_LETTER = 0;
|
||||
protected static final int GENRES = 1;
|
||||
protected static final int RANDOM = 2;
|
||||
protected static final int SUGGESTED = 4;
|
||||
protected static final int GRID = 6;
|
||||
protected static final int ALBUMS = 7;
|
||||
@@ -83,10 +81,12 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
protected static final int SERIES = 11;
|
||||
protected static final int ALBUM_ARTISTS = 12;
|
||||
protected BaseItemDto mFolder;
|
||||
protected BaseItemKind itemType;
|
||||
protected String itemTypeString;
|
||||
protected boolean showViews = true;
|
||||
protected boolean justLoaded = true;
|
||||
|
||||
protected BaseRowItem favSongsRowItem;
|
||||
|
||||
protected RowsSupportFragment mRowsFragment;
|
||||
protected CompositeClickedListener mClickedListener = new CompositeClickedListener();
|
||||
protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
|
||||
@@ -100,13 +100,11 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
private Lazy<MarkdownRenderer> markdownRenderer = inject(MarkdownRenderer.class);
|
||||
private final Lazy<CustomMessageRepository> customMessageRepository = inject(CustomMessageRepository.class);
|
||||
private final Lazy<NavigationRepository> navigationRepository = inject(NavigationRepository.class);
|
||||
private final Lazy<ApiClient> api = inject(ApiClient.class);
|
||||
private final Lazy<ItemLauncher> itemLauncher = inject(ItemLauncher.class);
|
||||
private final Lazy<KeyProcessor> keyProcessor = inject(KeyProcessor.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
favSongsRowItem = new BaseRowItem(FakeBaseItem.INSTANCE.getFAV_SONGS());
|
||||
|
||||
mRowsAdapter = new MutableObjectAdapter<Row>(new PositionableListRowPresenter());
|
||||
|
||||
@@ -164,16 +162,16 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
if (mFolder.getCollectionType() != null) {
|
||||
switch (mFolder.getCollectionType()) {
|
||||
case MOVIES:
|
||||
itemType = BaseItemKind.MOVIE;
|
||||
case CollectionType.Movies:
|
||||
itemTypeString = "Movie";
|
||||
break;
|
||||
case TVSHOWS:
|
||||
itemType = BaseItemKind.SERIES;
|
||||
case CollectionType.TvShows:
|
||||
itemTypeString = "Series";
|
||||
break;
|
||||
case MUSIC:
|
||||
itemType = BaseItemKind.MUSIC_ALBUM;
|
||||
case CollectionType.Music:
|
||||
itemTypeString = "MusicAlbum";
|
||||
break;
|
||||
case FOLDERS:
|
||||
case CollectionType.Folders:
|
||||
showViews = false;
|
||||
break;
|
||||
default:
|
||||
@@ -190,7 +188,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
|
||||
// React to deletion
|
||||
DataRefreshService dataRefreshService = KoinJavaComponent.<DataRefreshService>get(DataRefreshService.class);
|
||||
if (mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
if (mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getBaseItem().getId().equals(dataRefreshService.getLastDeletedItemId())) {
|
||||
((ItemRowAdapter) mCurrentRow.getAdapter()).remove(mCurrentItem);
|
||||
dataRefreshService.setLastDeletedItemId(null);
|
||||
}
|
||||
@@ -224,8 +222,8 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
mRowsAdapter = new MutableObjectAdapter<Row>(new PositionableListRowPresenter());
|
||||
mCardPresenter = new CardPresenter(false, 140);
|
||||
ClassPresenterSelector ps = new ClassPresenterSelector();
|
||||
ps.addClassPresenter(GridButtonBaseRowItem.class, new GridButtonPresenter(155, 140));
|
||||
ps.addClassPresenter(BaseRowItem.class, mCardPresenter);
|
||||
ps.addClassPresenter(GridButton.class, new GridButtonPresenter(155, 140));
|
||||
|
||||
for (BrowseRowDef def : rows) {
|
||||
HeaderItem header = new HeaderItem(def.getHeaderText());
|
||||
@@ -237,8 +235,14 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case LatestItems:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getLatestItemsQuery(), true, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Season:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSeasonQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Upcoming:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getUpcomingQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Views:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), GetUserViewsRequest.INSTANCE, mCardPresenter, mRowsAdapter);
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), ViewQuery.INSTANCE, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case SimilarSeries:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, mRowsAdapter);
|
||||
@@ -246,6 +250,9 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case SimilarMovies:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Persons:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getPersonsQuery(), def.getChunkSize(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case LiveTvChannel:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getTvChannelQuery(), 40, mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
@@ -261,9 +268,6 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case SeriesTimer:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSeriesTimerQuery(), mCardPresenter, mRowsAdapter);
|
||||
break;
|
||||
case Specials:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getSpecialsQuery(), new CardPresenter(true, ImageType.THUMB, 150), mRowsAdapter);
|
||||
break;
|
||||
default:
|
||||
rowAdapter = new ItemRowAdapter(requireContext(), def.getQuery(), def.getChunkSize(), def.getPreferParentThumb(), def.isStaticHeight(), ps, mRowsAdapter, def.getQueryType());
|
||||
break;
|
||||
@@ -283,30 +287,23 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
}
|
||||
|
||||
protected void addAdditionalRows(MutableObjectAdapter<Row> rowAdapter) {
|
||||
if (!showViews || itemType == null) return;
|
||||
if (!showViews) return;
|
||||
|
||||
HeaderItem gridHeader = new HeaderItem(rowAdapter.size(), getString(R.string.lbl_views));
|
||||
GridButtonPresenter mGridPresenter = new GridButtonPresenter();
|
||||
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter);
|
||||
|
||||
switch (itemType) {
|
||||
case MOVIE:
|
||||
switch (itemTypeString) {
|
||||
case "Movie":
|
||||
gridRowAdapter.add(new GridButton(SUGGESTED, getString(R.string.lbl_suggested)));
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
case MUSIC_ALBUM:
|
||||
case "MusicAlbum":
|
||||
gridRowAdapter.add(new GridButton(ALBUMS, getString(R.string.lbl_albums)));
|
||||
gridRowAdapter.add(new GridButton(ALBUM_ARTISTS, getString(R.string.lbl_album_artists)));
|
||||
gridRowAdapter.add(new GridButton(ARTISTS, getString(R.string.lbl_artists)));
|
||||
gridRowAdapter.add(new GridButton(GENRES, getString(R.string.lbl_genres)));
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
case SERIES:
|
||||
addStandardViewButtons(gridRowAdapter);
|
||||
gridRowAdapter.add(new GridButton(RANDOM, getString(R.string.random)));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -342,18 +339,33 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() != KeyEvent.ACTION_UP) return false;
|
||||
return keyProcessor.getValue().handleKey(keyCode, mCurrentItem, requireActivity());
|
||||
return KeyProcessor.HandleKey(keyCode, mCurrentItem, requireActivity());
|
||||
}
|
||||
|
||||
private void refreshCurrentItem() {
|
||||
if (mCurrentRow == null || mCurrentItem == null) return;
|
||||
ItemRowAdapterHelperKt.refreshItem((ItemRowAdapter) mCurrentRow.getAdapter(), api.getValue(), this, mCurrentItem);
|
||||
if (mCurrentItem != null &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.PHOTO &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ARTIST &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.MUSIC_ALBUM &&
|
||||
mCurrentItem.getBaseItemType() != BaseItemKind.PLAYLIST
|
||||
) {
|
||||
BaseRowItem item = mCurrentItem;
|
||||
item.refresh(new LifecycleAwareResponse<BaseItemDto>(getLifecycle()) {
|
||||
@Override
|
||||
public void onResponse(BaseItemDto response) {
|
||||
if (!getActive()) return;
|
||||
|
||||
ItemRowAdapter adapter = (ItemRowAdapter) mCurrentRow.getAdapter();
|
||||
if (response == null) adapter.removeAt(adapter.indexOf(item), 1);
|
||||
else adapter.notifyItemRangeChanged(adapter.indexOf(item), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class SpecialViewClickedListener implements OnItemViewClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButtonBaseRowItem) item = ((GridButtonBaseRowItem) item).getGridButton();
|
||||
if (item instanceof GridButton) {
|
||||
switch (((GridButton) item).getId()) {
|
||||
case GRID:
|
||||
@@ -363,7 +375,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
case ALBUMS:
|
||||
mFolder = JavaCompat.copyWithDisplayPreferencesId(mFolder, mFolder.getId() + "AL");
|
||||
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, BaseItemKind.MUSIC_ALBUM.getSerialName()));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryBrowser(mFolder, "MusicAlbum"));
|
||||
break;
|
||||
|
||||
case ALBUM_ARTISTS:
|
||||
@@ -379,25 +391,11 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
break;
|
||||
|
||||
case BY_LETTER:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByLetter(mFolder, itemType.getSerialName()));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByLetter(mFolder, itemTypeString));
|
||||
break;
|
||||
|
||||
case GENRES:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByGenres(mFolder, itemType.getSerialName()));
|
||||
break;
|
||||
|
||||
case RANDOM:
|
||||
BrowsingUtils.getRandomItem(api.getValue(), getViewLifecycleOwner(), mFolder, itemType, randomItem -> {
|
||||
if (randomItem != null) {
|
||||
if (randomItem.getType() == BaseItemKind.MUSIC_ALBUM) {
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemList(randomItem.getId()));
|
||||
} else {
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(randomItem.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.libraryByGenres(mFolder, itemTypeString));
|
||||
break;
|
||||
|
||||
case SUGGESTED:
|
||||
@@ -405,12 +403,12 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
break;
|
||||
|
||||
case FAVSONGS:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.musicFavorites(mFolder.getId()));
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemList(FakeBaseItem.INSTANCE.getFAV_SONGS_ID(), mFolder.getId()));
|
||||
break;
|
||||
|
||||
case SERIES:
|
||||
case LiveTvOption.LIVE_TV_SERIES_OPTION_ID:
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.getLiveTvSeriesRecordings());
|
||||
navigationRepository.getValue().navigate(Destinations.INSTANCE.librarySmartScreen(FakeBaseItem.INSTANCE.getSERIES_TIMERS()));
|
||||
break;
|
||||
|
||||
case SCHEDULE:
|
||||
@@ -439,7 +437,7 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (!(item instanceof BaseRowItem)) return;
|
||||
|
||||
itemLauncher.getValue().launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), requireContext());
|
||||
ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow) row).getAdapter(), ((BaseRowItem) item).getIndex(), requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,12 +445,15 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
@Override
|
||||
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
|
||||
RowPresenter.ViewHolder rowViewHolder, Row row) {
|
||||
if (item instanceof GridButton && ((GridButton) item).getId() == FAVSONGS) {
|
||||
// Set to specialized item
|
||||
mCurrentItem = favSongsRowItem;
|
||||
}
|
||||
|
||||
if (!(item instanceof BaseRowItem)) {
|
||||
mTitle.setText(mFolder != null ? mFolder.getName() : "");
|
||||
mInfoRow.removeAllViews();
|
||||
mSummary.setText("");
|
||||
mCurrentItem = null;
|
||||
mCurrentRow = null;
|
||||
// Fill in default background
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
return;
|
||||
@@ -471,10 +472,10 @@ public class EnhancedBrowseFragment extends Fragment implements RowLoader, View.
|
||||
mSummary.setText(markdownRenderer.getValue().toMarkdownSpanned(summary));
|
||||
else mSummary.setText(null);
|
||||
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), rowItem.getBaseItem(), mInfoRow, true);
|
||||
InfoLayoutHelper.addInfoRow(requireContext(), rowItem, mInfoRow, true, true);
|
||||
|
||||
ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow) row).getAdapter();
|
||||
adapter.loadMoreItemsIfNeeded(adapter.indexOf(rowItem));
|
||||
adapter.loadMoreItemsIfNeeded(rowItem.getIndex());
|
||||
|
||||
backgroundService.getValue().setBackground(rowItem.getBaseItem());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.jellyfin.androidtv.ui.browsing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.jellyfin.androidtv.R;
|
||||
import org.jellyfin.androidtv.data.querying.StdItemQuery;
|
||||
import org.jellyfin.androidtv.util.Utils;
|
||||
import org.jellyfin.apiclient.model.entities.SortOrder;
|
||||
import org.jellyfin.apiclient.model.querying.ItemFilter;
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind;
|
||||
import org.jellyfin.sdk.model.constant.ItemSortBy;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class GenericFolderFragment extends EnhancedBrowseFragment {
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
}
|
||||
|
||||
private static BaseItemKind[] showSpecialViewTypes = new BaseItemKind[]{
|
||||
BaseItemKind.COLLECTION_FOLDER,
|
||||
BaseItemKind.FOLDER,
|
||||
BaseItemKind.USER_VIEW,
|
||||
BaseItemKind.CHANNEL_FOLDER_ITEM
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void setupQueries(RowLoader rowLoader) {
|
||||
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0 ||
|
||||
mFolder.getType() == BaseItemKind.CHANNEL ||
|
||||
mFolder.getType() == BaseItemKind.CHANNEL_FOLDER_ITEM ||
|
||||
mFolder.getType() == BaseItemKind.USER_VIEW ||
|
||||
mFolder.getType() == BaseItemKind.COLLECTION_FOLDER) {
|
||||
boolean showSpecialViews = Arrays.asList(showSpecialViewTypes).contains(mFolder.getType());
|
||||
|
||||
if (showSpecialViews) {
|
||||
if (mFolder.getType() != BaseItemKind.CHANNEL_FOLDER_ITEM) {
|
||||
StdItemQuery resume = new StdItemQuery();
|
||||
resume.setParentId(mFolder.getId().toString());
|
||||
resume.setLimit(50);
|
||||
resume.setFilters(new ItemFilter[]{ItemFilter.IsResumable});
|
||||
resume.setSortBy(new String[]{ItemSortBy.DatePlayed});
|
||||
resume.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_continue_watching), resume, 0));
|
||||
}
|
||||
|
||||
StdItemQuery latest = new StdItemQuery();
|
||||
latest.setParentId(mFolder.getId().toString());
|
||||
latest.setLimit(50);
|
||||
latest.setFilters(new ItemFilter[]{ItemFilter.IsUnplayed});
|
||||
latest.setSortBy(new String[]{ItemSortBy.DateCreated});
|
||||
latest.setSortOrder(SortOrder.Descending);
|
||||
mRows.add(new BrowseRowDef(getString(R.string.lbl_latest), latest, 0));
|
||||
|
||||
}
|
||||
|
||||
StdItemQuery byName = new StdItemQuery();
|
||||
byName.setParentId(mFolder.getId().toString());
|
||||
String header = (mFolder.getType() == BaseItemKind.SEASON) ? mFolder.getName() : getString(R.string.lbl_by_name);
|
||||
mRows.add(new BrowseRowDef(header, byName, 100));
|
||||
|
||||
rowLoader.loadRows(mRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.jellyfin.androidtv.ui.browsing
|
||||
|
||||
import org.jellyfin.androidtv.R
|
||||
import org.jellyfin.androidtv.data.querying.GetSpecialsRequest
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
|
||||
class GenericFolderFragment : EnhancedBrowseFragment() {
|
||||
companion object {
|
||||
private val showSpecialViewTypes = setOf(
|
||||
BaseItemKind.COLLECTION_FOLDER,
|
||||
BaseItemKind.FOLDER,
|
||||
BaseItemKind.USER_VIEW,
|
||||
BaseItemKind.CHANNEL_FOLDER_ITEM,
|
||||
)
|
||||
|
||||
private val itemFields = setOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.ITEM_COUNTS,
|
||||
ItemFields.DISPLAY_PREFERENCES_ID,
|
||||
ItemFields.CHILD_COUNT,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setupQueries(rowLoader: RowLoader) {
|
||||
if ((mFolder.childCount == null || mFolder.childCount == 0) && !setOf(
|
||||
BaseItemKind.CHANNEL,
|
||||
BaseItemKind.CHANNEL_FOLDER_ITEM,
|
||||
BaseItemKind.USER_VIEW,
|
||||
BaseItemKind.COLLECTION_FOLDER,
|
||||
).contains(mFolder.type)
|
||||
) return
|
||||
|
||||
if (showSpecialViewTypes.contains(mFolder.type)) {
|
||||
if (mFolder.type != BaseItemKind.CHANNEL_FOLDER_ITEM) {
|
||||
val resume = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
limit = 50,
|
||||
filters = setOf(ItemFilter.IS_RESUMABLE),
|
||||
sortBy = setOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_continue_watching), resume, 0))
|
||||
}
|
||||
|
||||
val latest = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
limit = 50,
|
||||
filters = setOf(ItemFilter.IS_UNPLAYED),
|
||||
sortBy = setOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = setOf(SortOrder.DESCENDING),
|
||||
)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_latest), latest, 0))
|
||||
}
|
||||
|
||||
val byName = GetItemsRequest(
|
||||
fields = itemFields,
|
||||
parentId = mFolder.id,
|
||||
)
|
||||
val header = when (mFolder.type) {
|
||||
BaseItemKind.SEASON -> mFolder.name
|
||||
else -> getString(R.string.lbl_by_name)
|
||||
}
|
||||
|
||||
mRows.add(BrowseRowDef(header, byName, 100))
|
||||
|
||||
if (mFolder.type == BaseItemKind.SEASON) {
|
||||
val specials = GetSpecialsRequest(mFolder.id)
|
||||
mRows.add(BrowseRowDef(getString(R.string.lbl_specials), specials))
|
||||
}
|
||||
|
||||
rowLoader.loadRows(mRows)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user