Merge branch 'jarnedemeulemeester:main' into auto-offline-mode

This commit is contained in:
Freya 2024-06-24 13:32:53 +00:00 committed by GitHub
commit e74a86da24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
245 changed files with 4344 additions and 1136 deletions

View file

@ -12,14 +12,14 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew lintDebug ktlintCheck run: ./gradlew lintDebug ktlintCheck
assemble: assemble:
@ -29,14 +29,14 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew assembleDebug run: ./gradlew assembleDebug
# Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged. # Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged.

65
.github/workflows/publish.yaml vendored Normal file
View file

@ -0,0 +1,65 @@
name: Publish
on:
push:
tags:
- v*
jobs:
publish:
name: Publish
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3
- name: Decode keystore file
uses: timheuer/base64-to-file@v1
id: findroid_keystore
with:
fileName: 'findroid-keystore.jks'
encodedString: ${{ secrets.FINDROID_KEYSTORE }}
- name: Decode Play API credentials file
uses: timheuer/base64-to-file@v1
id: findroid_play_api_credentials
with:
fileName: 'findroid-play-api-credentials.json'
encodedString: ${{ secrets.FINDROID_PLAY_API_CREDENTIALS }}
- name: Build and publish
run: bundle exec fastlane publish
env:
FINDROID_KEYSTORE: ${{ steps.findroid_keystore.outputs.filePath }}
FINDROID_KEYSTORE_PASSWORD: ${{ secrets.FINDROID_KEYSTORE_PASSWORD }}
FINDROID_KEY_ALIAS: ${{ secrets.FINDROID_KEY_ALIAS }}
FINDROID_KEY_PASSWORD: ${{ secrets.FINDROID_KEY_PASSWORD }}
FINDROID_PLAY_API_CREDENTIALS: ${{ steps.findroid_play_api_credentials.outputs.filePath }}
- name: Create release
uses: softprops/action-gh-release@v2
with:
draft: true
files: |
./app/phone/build/outputs/apk/libre/release/findroid-${{ github.ref_name }}-libre-arm64-v8a.apk
./app/phone/build/outputs/apk/libre/release/findroid-${{ github.ref_name }}-libre-armeabi-v7a.apk
./app/phone/build/outputs/apk/libre/release/findroid-${{ github.ref_name }}-libre-x86_64.apk
./app/phone/build/outputs/apk/libre/release/findroid-${{ github.ref_name }}-libre-x86.apk

6
.gitignore vendored
View file

@ -31,3 +31,9 @@ google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
# Fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

3
Gemfile Normal file
View file

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

218
Gemfile.lock Normal file
View file

@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.4.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.4

View file

@ -1,6 +1,12 @@
![Findroid banner](images/findroid-banner.png) ![Findroid banner](images/findroid-banner.png)
# Findroid # Findroid
![GitHub release (with filter)](https://img.shields.io/github/v/release/jarnedemeulemeester/findroid?style=for-the-badge)
![GitHub repo stars](https://img.shields.io/github/stars/jarnedemeulemeester/findroid?style=for-the-badge)
![GitHub issues](https://img.shields.io/github/issues/jarnedemeulemeester/findroid?style=for-the-badge)
![GitHub pull requests](https://img.shields.io/github/issues-pr/jarnedemeulemeester/findroid?style=for-the-badge)
![GitHub all releases](https://img.shields.io/github/downloads/jarnedemeulemeester/findroid/total?style=for-the-badge)
![GitHub](https://img.shields.io/github/license/jarnedemeulemeester/findroid?style=for-the-badge)
Findroid is third-party Android application for Jellyfin that provides a native user interface to browse and play movies and series. Findroid is third-party Android application for Jellyfin that provides a native user interface to browse and play movies and series.
@ -8,7 +14,7 @@ I am developing this application in my spare time.
**This project is in its early stages so expect bugs.** **This project is in its early stages so expect bugs.**
<a href='https://play.google.com/store/apps/details?id=dev.jdtech.jellyfin'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height="80"/></a><a href='http://www.amazon.com/gp/product/B0BTWC8DNZ'><img alt='Available at Amazon Appstore' src='https://user-images.githubusercontent.com/32322857/219019331-027a6775-7362-44bb-a026-281f71e9b37b.png' height="80"/></a><a href='https://appgallery.huawei.com/app/C107646987'><img alt='Explore it on Huawei AppGallery' src='https://user-images.githubusercontent.com/32322857/219013519-f0a11e17-c32c-42fd-8009-ab77fe2c23e7.png' height="80"/></a><a href='https://apt.izzysoft.de/fdroid/index/apk/dev.jdtech.jellyfin'><img alt='Get it on IzzyOnDroid' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' height="80"/></a> <a href='https://play.google.com/store/apps/details?id=dev.jdtech.jellyfin'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height="80"/></a><a href='http://www.amazon.com/gp/product/B0BTWC8DNZ'><img alt='Available at Amazon Appstore' src='https://user-images.githubusercontent.com/32322857/219019331-027a6775-7362-44bb-a026-281f71e9b37b.png' height="80"/></a><a href='https://apt.izzysoft.de/fdroid/index/apk/dev.jdtech.jellyfin'><img alt='Get it on IzzyOnDroid' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' height="80"/></a>
## Screenshots ## Screenshots
| Home | Library | Movie | Season | Episode | | Home | Library | Movie | Season | Episode |
@ -23,7 +29,7 @@ I am developing this application in my spare time.
- ExoPlayer - ExoPlayer
- Video codecs: H.263, H.264, H.265, VP8, VP9, AV1 - Video codecs: H.263, H.264, H.265, VP8, VP9, AV1
- Support depends on Android device - Support depends on Android device
- Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD - Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD
- Support provided by ExoPlayer FFmpeg extension - Support provided by ExoPlayer FFmpeg extension
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB - Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435) - SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435)
@ -34,6 +40,9 @@ I am developing this application in my spare time.
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB - Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
- Optionally force software decoding when hardware decoding has issues. - Optionally force software decoding when hardware decoding has issues.
- Picture-in-picture mode - Picture-in-picture mode
- Media chapters
- Timeline markers
- Chapter navigation gestures
## Planned features ## Planned features
- Android TV - Android TV

View file

@ -21,6 +21,20 @@ android {
versionCode = Versions.appCode versionCode = Versions.appCode
versionName = Versions.appName versionName = Versions.appName
testInstrumentationRunner = "dev.jdtech.jellyfin.HiltTestRunner"
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
if (variant.buildType.name == "release") {
val outputFileName = "findroid-v${variant.versionName}-${variant.flavorName}-${output.getFilter("ABI")}.apk"
output.outputFileName = outputFileName
}
}
} }
buildTypes { buildTypes {
@ -47,9 +61,6 @@ android {
dimension = "variant" dimension = "variant"
isDefault = true isDefault = true
} }
register("huawei") {
dimension = "variant"
}
} }
splits { splits {
@ -61,6 +72,8 @@ android {
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = Versions.java sourceCompatibility = Versions.java
targetCompatibility = Versions.java targetCompatibility = Versions.java
} }
@ -78,11 +91,11 @@ ktlint {
} }
dependencies { dependencies {
implementation(project(":core")) implementation(projects.core)
implementation(project(":data")) implementation(projects.data)
implementation(project(":preferences")) implementation(projects.preferences)
implementation(project(":player:core")) implementation(projects.player.core)
implementation(project(":player:video")) implementation(projects.player.video)
implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries) implementation(libs.aboutlibraries)
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
@ -90,9 +103,7 @@ dependencies {
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.session)
implementation(libs.androidx.navigation.fragment) implementation(libs.androidx.navigation.fragment)
@ -109,7 +120,14 @@ dependencies {
implementation(libs.jellyfin.core) implementation(libs.jellyfin.core)
compileOnly(libs.libmpv) compileOnly(libs.libmpv)
implementation(libs.material) implementation(libs.material)
implementation(libs.media3.ffmpeg.decoder)
implementation(libs.timber) implementation(libs.timber)
implementation(rootProject.files("libs/lib-decoder-ffmpeg-release.aar")) coreLibraryDesugaring(libs.android.desugar.jdk)
androidTestImplementation(libs.androidx.room.runtime)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.hilt.android.testing)
kspTest(libs.hilt.android.compiler)
} }

View file

@ -0,0 +1,16 @@
package dev.jdtech.jellyfin
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?,
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

View file

@ -0,0 +1,95 @@
package dev.jdtech.jellyfin
import android.util.Log
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dev.jdtech.jellyfin.di.DatabaseModule
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
@UninstallModules(DatabaseModule::class)
class MainActivityTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Before
fun setUp() {
hiltRule.inject()
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun testMainFlow() {
launchActivity<MainActivity>().use {
// Wait for the app to load
waitForElement(allOf(withId(R.id.edit_text_server_address), isDisplayed()))
// Connect to demo server
onView(withId(R.id.edit_text_server_address)).perform(typeText("https://demo.jellyfin.org/stable"), closeSoftKeyboard())
onView(withId(R.id.button_connect)).perform(click())
// Connecting to the server
waitForElement(allOf(withId(R.id.edit_text_username), isDisplayed()))
// Login
onView(withId(R.id.edit_text_username)).perform(typeText("demo"), closeSoftKeyboard())
onView(withId(R.id.button_login)).perform(click())
// Navigate to My media
waitForElement(allOf(withText("Continue Watching"), isDisplayed()))
onView(withId(R.id.mediaFragment)).perform(click())
// Navigate to movies
waitForElement(allOf(withText("Movies"), isDisplayed()))
onView(withText("Movies")).perform(click())
// Navigate to Battle of the Stars
waitForElement(allOf(withText("Battle of the Stars"), isDisplayed()))
onView(withText("Battle of the Stars")).perform(click())
// Play the movie
waitForElement(allOf(withId(R.id.play_button), isEnabled()))
onView(withId(R.id.play_button)).perform(click())
// Wait for movie to start playing
waitForElement(allOf(withId(androidx.media3.ui.R.id.exo_buffering), isDisplayed()))
waitForElement(allOf(withId(androidx.media3.ui.R.id.exo_buffering), not(isDisplayed())))
// Navigate back
Espresso.pressBack()
}
}
}

View file

@ -0,0 +1,64 @@
package dev.jdtech.jellyfin
import android.view.View
import android.view.ViewTreeObserver
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.hamcrest.StringDescription
private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener {
private lateinit var callback: IdlingResource.ResourceCallback
private var matched = false
override fun getName() = "View property change callback"
override fun isIdleNow() = matched
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
override fun onDraw() {
matched = matcher.matches(view)
callback.onTransitionToIdle()
}
}
fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction {
override fun getConstraints(): Matcher<View> {
return CoreMatchers.any(View::class.java)
}
override fun getDescription(): String {
return StringDescription().let {
matcher.describeTo(it)
"wait until: $it"
}
}
override fun perform(uiController: UiController, view: View) {
if (!matcher.matches(view)) {
ViewPropertyChangeCallback(matcher, view).run {
try {
IdlingRegistry.getInstance().register(this)
view.viewTreeObserver.addOnDrawListener(this)
uiController.loopMainThreadUntilIdle()
} finally {
view.viewTreeObserver.removeOnDrawListener(this)
IdlingRegistry.getInstance().unregister(this)
}
}
}
}
}
fun waitForElement(matcher: Matcher<View>) {
onView(isRoot()).perform(waitUntil(hasDescendant(matcher)))
}

View file

@ -0,0 +1,29 @@
package dev.jdtech.jellyfin.di
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.jdtech.jellyfin.database.ServerDatabase
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseTestModule {
@Singleton
@Provides
fun provideServerDatabaseDao(@ApplicationContext app: Context): ServerDatabaseDao {
return Room.inMemoryDatabaseBuilder(
app.applicationContext,
ServerDatabase::class.java,
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
.getServerDatabaseDao()
}
}

View file

@ -10,9 +10,11 @@ import coil.decode.SvgDecoder
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.request.CachePolicy import coil.request.CachePolicy
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import dev.jdtech.jellyfin.core.R as CoreR
@HiltAndroidApp @HiltAndroidApp
class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactory { class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactory {
@ -40,7 +42,12 @@ class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactor
"dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
} }
if (appPreferences.dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this) if (appPreferences.dynamicColors) {
val dynamicColorsOptions = DynamicColorsOptions.Builder()
.setThemeOverlay(CoreR.style.ThemeOverlay_Findroid_DynamicColors)
.build()
DynamicColors.applyToActivitiesIfAvailable(this, dynamicColorsOptions)
}
} }
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {

View file

@ -8,7 +8,6 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.media3.exoplayer.trackselection.MappingTrackSelector
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
@ -72,19 +71,6 @@ abstract class BasePlayerActivity : AppCompatActivity() {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} }
protected fun isRendererType(
mappedTrackInfo: MappingTrackSelector.MappedTrackInfo,
rendererIndex: Int,
type: Int,
): Boolean {
val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
if (trackGroupArray.length == 0) {
return false
}
val trackType = mappedTrackInfo.getRendererType(rendererIndex)
return type == trackType
}
protected fun configureInsets(playerControls: View) { protected fun configureInsets(playerControls: View) {
playerControls.setOnApplyWindowInsetsListener { _, windowInsets -> playerControls.setOnApplyWindowInsetsListener { _, windowInsets ->
val cutout = windowInsets.displayCutout val cutout = windowInsets.displayCutout

View file

@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity() {
private fun applyTheme() { private fun applyTheme() {
if (appPreferences.amoledTheme) { if (appPreferences.amoledTheme) {
setTheme(CoreR.style.Theme_FindroidAMOLED) setTheme(CoreR.style.ThemeOverlay_Findroid_Amoled)
} }
} }

View file

@ -7,11 +7,13 @@ import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect import android.graphics.Rect
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.provider.Settings
import android.util.Rational import android.util.Rational
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
@ -27,15 +29,14 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.navigation.navArgs import androidx.navigation.navArgs
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.utils.PlayerGestureHelper import dev.jdtech.jellyfin.utils.PlayerGestureHelper
import dev.jdtech.jellyfin.utils.PreviewScrubListener import dev.jdtech.jellyfin.utils.PreviewScrubListener
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
@ -56,6 +57,7 @@ class PlayerActivity : BasePlayerActivity() {
private var playerGestureHelper: PlayerGestureHelper? = null private var playerGestureHelper: PlayerGestureHelper? = null
override val viewModel: PlayerActivityViewModel by viewModels() override val viewModel: PlayerActivityViewModel by viewModels()
private var previewScrubListener: PreviewScrubListener? = null private var previewScrubListener: PreviewScrubListener? = null
private var wasZoom: Boolean = false
private val isPipSupported by lazy { private val isPipSupported by lazy {
// Check if device has PiP feature // Check if device has PiP feature
@ -112,10 +114,6 @@ class PlayerActivity : BasePlayerActivity() {
finish() finish()
} }
binding.playerView.findViewById<View>(R.id.back_button_alt).setOnClickListener {
finish()
}
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name) val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track) val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
@ -143,9 +141,21 @@ class PlayerActivity : BasePlayerActivity() {
} }
} }
// Trick Play // Trickplay
previewScrubListener?.let { previewScrubListener?.let {
it.currentTrickPlay = currentTrickPlay it.currentTrickplay = currentTrickplay
}
// Chapters
if (appPreferences.showChapterMarkers && currentChapters != null) {
currentChapters?.let { chapters ->
val playerControlView = findViewById<PlayerControlView>(R.id.exo_controller)
val numOfChapters = chapters.size
playerControlView.setExtraAdGroupMarkers(
LongArray(numOfChapters) { index -> chapters[index].startPosition },
BooleanArray(numOfChapters) { false },
)
}
} }
// File Loaded // File Loaded
@ -169,6 +179,13 @@ class PlayerActivity : BasePlayerActivity() {
viewModel.eventsChannelFlow.collect { event -> viewModel.eventsChannelFlow.collect { event ->
when (event) { when (event) {
is PlayerEvents.NavigateBack -> finish() is PlayerEvents.NavigateBack -> finish()
is PlayerEvents.IsPlayingChanged -> {
if (appPreferences.playerPipGesture) {
try {
setPictureInPictureParams(pipParams(event.isPlaying))
} catch (_: IllegalArgumentException) { }
}
}
} }
} }
} }
@ -238,9 +255,12 @@ class PlayerActivity : BasePlayerActivity() {
pictureInPicture() pictureInPicture()
} }
if (appPreferences.playerTrickPlay) { // Set marker color
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress) val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
timeBar.setAdMarkerColor(Color.WHITE)
if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener = PreviewScrubListener( previewScrubListener = PreviewScrubListener(
imagePreview, imagePreview,
timeBar, timeBar,
@ -254,7 +274,7 @@ class PlayerActivity : BasePlayerActivity() {
hideSystemUI() hideSystemUI()
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
@ -263,12 +283,17 @@ class PlayerActivity : BasePlayerActivity() {
} }
override fun onUserLeaveHint() { override fun onUserLeaveHint() {
if (appPreferences.playerPipGesture && viewModel.player.isPlaying && !isControlsLocked) { super.onUserLeaveHint()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
appPreferences.playerPipGesture &&
viewModel.player.isPlaying &&
!isControlsLocked
) {
pictureInPicture() pictureInPicture()
} }
} }
private fun pipParams(): PictureInPictureParams { private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height) val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
val aspectRatio = binding.playerView.player?.videoSize?.let { val aspectRatio = binding.playerView.player?.videoSize?.let {
@ -296,24 +321,21 @@ class PlayerActivity : BasePlayerActivity() {
) )
} }
return PictureInPictureParams.Builder() val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio) .setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint) .setSourceRectHint(sourceRectHint)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(enableAutoEnter)
}
return builder.build()
} }
private fun pictureInPicture() { private fun pictureInPicture() {
if (!isPipSupported) { if (!isPipSupported) {
return return
} }
binding.playerView.useController = false
binding.playerView.findViewById<Button>(R.id.btn_skip_intro).isVisible = false
if (binding.playerView.player is MPVPlayer) {
(binding.playerView.player as MPVPlayer).updateZoomMode(false)
} else {
binding.playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
}
try { try {
enterPictureInPictureMode(pipParams()) enterPictureInPictureMode(pipParams())
@ -325,8 +347,35 @@ class PlayerActivity : BasePlayerActivity() {
newConfig: Configuration, newConfig: Configuration,
) { ) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (!isInPictureInPictureMode) { when (isInPictureInPictureMode) {
true -> {
binding.playerView.useController = false
binding.playerView.findViewById<Button>(R.id.btn_skip_intro).isVisible = false
wasZoom = playerGestureHelper?.isZoomEnabled ?: false
playerGestureHelper?.updateZoomMode(false)
// Brightness mode Auto
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
}
false -> {
binding.playerView.useController = true binding.playerView.useController = true
playerGestureHelper?.updateZoomMode(wasZoom)
// Override auto brightness
window.attributes = window.attributes.apply {
screenBrightness = if (appPreferences.playerBrightnessRemember) {
appPreferences.playerBrightness
} else {
Settings.System.getInt(
contentResolver,
Settings.System.SCREEN_BRIGHTNESS,
).toFloat() / 255
}
}
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
package dev.jdtech.jellyfin.adapters package dev.jdtech.jellyfin.adapters
import android.text.Html.fromHtml
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -45,7 +46,7 @@ class EpisodeListAdapter(
binding.root.context.getString(CoreR.string.episode_name_with_end, episode.indexNumber, episode.indexNumberEnd, episode.name) binding.root.context.getString(CoreR.string.episode_name_with_end, episode.indexNumber, episode.indexNumberEnd, episode.name)
} }
binding.episodeOverview.text = episode.overview binding.episodeOverview.text = fromHtml(episode.overview, 0)
if (episode.playbackPositionTicks > 0) { if (episode.playbackPositionTicks > 0) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension( binding.progressBar.layoutParams.width = TypedValue.applyDimension(

View file

@ -24,6 +24,7 @@ import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.CollectionViewModel import dev.jdtech.jellyfin.viewmodels.CollectionViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import dev.jdtech.jellyfin.core.R as CoreR
@AndroidEntryPoint @AndroidEntryPoint
class CollectionFragment : Fragment() { class CollectionFragment : Fragment() {
@ -40,6 +41,8 @@ class CollectionFragment : Fragment() {
): View { ): View {
binding = FragmentFavoriteBinding.inflate(inflater, container, false) binding = FragmentFavoriteBinding.inflate(inflater, container, false)
binding.noFavoritesText.text = getString(CoreR.string.collection_no_media)
binding.favoritesRecyclerView.adapter = FavoritesListAdapter { item -> binding.favoritesRecyclerView.adapter = FavoritesListAdapter { item ->
navigateToMediaItem(item) navigateToMediaItem(item)
} }

View file

@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.fragments
import android.app.DownloadManager import android.app.DownloadManager
import android.os.Bundle import android.os.Bundle
import android.text.Html.fromHtml
import android.text.format.Formatter import android.text.format.Formatter
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
@ -285,11 +286,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
} }
binding.seriesName.text = episode.seriesName binding.seriesName.text = episode.seriesName
binding.overview.text = episode.overview binding.overview.text = fromHtml(episode.overview, 0)
binding.year.text = formatDateTime(episode.premiereDate) binding.year.text = formatDateTime(episode.premiereDate)
binding.playtime.text = getString(CoreR.string.runtime_minutes, episode.runtimeTicks.div(600000000)) binding.playtime.text = getString(CoreR.string.runtime_minutes, episode.runtimeTicks.div(600000000))
binding.communityRating.isVisible = episode.communityRating != null episode.communityRating?.also {
binding.communityRating.text = episode.communityRating.toString() binding.communityRating.text = episode.communityRating.toString()
binding.communityRating.isVisible = true
}
binding.missingIcon.isVisible = false binding.missingIcon.isVisible = false
if (appPreferences.displayExtraInfo) { if (appPreferences.displayExtraInfo) {

View file

@ -25,6 +25,7 @@ import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.SortDialogFragment import dev.jdtech.jellyfin.dialogs.SortDialogFragment
import dev.jdtech.jellyfin.models.FindroidBoxSet import dev.jdtech.jellyfin.models.FindroidBoxSet
import dev.jdtech.jellyfin.models.FindroidFolder
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
@ -222,6 +223,15 @@ class LibraryFragment : Fragment() {
), ),
) )
} }
is FindroidFolder -> {
findNavController().navigate(
LibraryFragmentDirections.actionLibraryFragmentSelf(
item.id,
item.name,
args.libraryType,
),
)
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.os.Bundle import android.os.Bundle
import android.text.Html.fromHtml
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -82,7 +83,7 @@ class LoginFragment : Fragment() {
viewModel.uiState.collect { uiState -> viewModel.uiState.collect { uiState ->
Timber.d("$uiState") Timber.d("$uiState")
when (uiState) { when (uiState) {
is LoginViewModel.UiState.Normal -> bindUiStateNormal() is LoginViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is LoginViewModel.UiState.Error -> bindUiStateError(uiState) is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
is LoginViewModel.UiState.Loading -> bindUiStateLoading() is LoginViewModel.UiState.Loading -> bindUiStateLoading()
} }
@ -135,11 +136,15 @@ class LoginFragment : Fragment() {
return binding.root return binding.root
} }
private fun bindUiStateNormal() { private fun bindUiStateNormal(uiState: LoginViewModel.UiState.Normal) {
binding.buttonLogin.isEnabled = true binding.buttonLogin.isEnabled = true
binding.progressCircular.isVisible = false binding.progressCircular.isVisible = false
binding.editTextUsernameLayout.isEnabled = true binding.editTextUsernameLayout.isEnabled = true
binding.editTextPasswordLayout.isEnabled = true binding.editTextPasswordLayout.isEnabled = true
uiState.disclaimer?.let { disclaimer ->
binding.loginDisclaimer.text = fromHtml(disclaimer, 0)
}
} }
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) { private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {

View file

@ -4,6 +4,7 @@ import android.app.DownloadManager
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Html.fromHtml
import android.text.format.Formatter import android.text.format.Formatter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -276,7 +277,6 @@ class MovieFragment : Fragment() {
if (item.trailer != null) { if (item.trailer != null) {
binding.itemActions.trailerButton.isVisible = true binding.itemActions.trailerButton.isVisible = true
} }
binding.communityRating.isVisible = item.communityRating != null
binding.actors.isVisible = actors.isNotEmpty() binding.actors.isVisible = actors.isNotEmpty()
binding.itemActions.playButton.isEnabled = item.canPlay && item.sources.isNotEmpty() binding.itemActions.playButton.isEnabled = item.canPlay && item.sources.isNotEmpty()
@ -309,7 +309,10 @@ class MovieFragment : Fragment() {
binding.playtime.text = runTime binding.playtime.text = runTime
} }
binding.officialRating.text = item.officialRating binding.officialRating.text = item.officialRating
binding.communityRating.text = item.communityRating.toString() item.communityRating?.also {
binding.communityRating.text = it.toString()
binding.communityRating.isVisible = true
}
videoMetadata.let { videoMetadata.let {
with(binding) { with(binding) {
@ -322,8 +325,8 @@ class MovieFragment : Fragment() {
it.displayProfiles.firstOrNull()?.apply { it.displayProfiles.firstOrNull()?.apply {
videoProfileChip.text = this.raw videoProfileChip.text = this.raw
videoProfileChip.isVisible = when (this) { videoProfileChip.isVisible = when (this) {
DisplayProfile.HDR,
DisplayProfile.HDR10, DisplayProfile.HDR10,
DisplayProfile.HDR10_PLUS,
DisplayProfile.HLG, DisplayProfile.HLG,
-> { -> {
videoProfileChip.chipStartPadding = .0f videoProfileChip.chipStartPadding = .0f
@ -379,7 +382,7 @@ class MovieFragment : Fragment() {
binding.info.sizeGroup.isVisible = size != null binding.info.sizeGroup.isVisible = size != null
} }
binding.info.description.text = item.overview binding.info.description.text = fromHtml(item.overview, 0)
binding.info.genres.text = genresString binding.info.genres.text = genresString
binding.info.genresGroup.isVisible = item.genres.isNotEmpty() binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
binding.info.director.text = director?.name binding.info.director.text = director?.name

View file

@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.fragments
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Html.fromHtml
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -170,7 +171,6 @@ class ShowFragment : Fragment() {
if (item.trailer != null) { if (item.trailer != null) {
binding.itemActions.trailerButton.isVisible = true binding.itemActions.trailerButton.isVisible = true
} }
binding.communityRating.isVisible = item.communityRating != null
binding.actors.isVisible = actors.isNotEmpty() binding.actors.isVisible = actors.isNotEmpty()
// TODO currently the sources of a show is always empty, we need a way to check if sources are available // TODO currently the sources of a show is always empty, we need a way to check if sources are available
@ -212,9 +212,12 @@ class ShowFragment : Fragment() {
binding.playtime.text = runTime binding.playtime.text = runTime
} }
binding.officialRating.text = item.officialRating binding.officialRating.text = item.officialRating
item.communityRating?.also {
binding.communityRating.text = item.communityRating.toString() binding.communityRating.text = item.communityRating.toString()
binding.communityRating.isVisible = true
}
binding.info.description.text = item.overview binding.info.description.text = fromHtml(item.overview, 0)
binding.info.genres.text = genresString binding.info.genres.text = genresString
binding.info.genresGroup.isVisible = item.genres.isNotEmpty() binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
binding.info.director.text = director?.name binding.info.director.text = director?.name

View file

@ -23,6 +23,7 @@ import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.Constants import dev.jdtech.jellyfin.Constants
import dev.jdtech.jellyfin.PlayerActivity import dev.jdtech.jellyfin.PlayerActivity
import dev.jdtech.jellyfin.isControlsLocked import dev.jdtech.jellyfin.isControlsLocked
import dev.jdtech.jellyfin.models.PlayerChapter
import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.MPVPlayer
import timber.log.Timber import timber.log.Timber
import kotlin.math.abs import kotlin.math.abs
@ -37,7 +38,7 @@ class PlayerGestureHelper(
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides. * Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
* Useful on wide-screen phones to remove black bars from some movies. * Useful on wide-screen phones to remove black bars from some movies.
*/ */
private var isZoomEnabled = false var isZoomEnabled = false
/** /**
* Tracks a value during a swipe gesture (between multiple onScroll calls). * Tracks a value during a swipe gesture (between multiple onScroll calls).
@ -55,9 +56,14 @@ class PlayerGestureHelper(
private var lastScaleEvent: Long = 0 private var lastScaleEvent: Long = 0
private var playbackSpeedIncrease: Float = 2f
private var lastPlaybackSpeed: Float = 0f
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
private var currentNumberOfPointers: Int = 0
private val tapGestureDetector = GestureDetector( private val tapGestureDetector = GestureDetector(
playerView.context, playerView.context,
object : GestureDetector.SimpleOnGestureListener() { object : GestureDetector.SimpleOnGestureListener() {
@ -69,6 +75,22 @@ class PlayerGestureHelper(
return true return true
} }
override fun onLongPress(e: MotionEvent) {
// Disables long press gesture if view is locked
if (isControlsLocked) return
// Stop long press gesture when more than 1 pointer
if (currentNumberOfPointers > 1) return
// This is a temporary solution for chapter skipping.
// TODO: Remove this after implementing #636
if (appPreferences.playerGesturesChapterSkip) {
handleChapterSkip(e)
} else {
enableSpeedIncrease()
}
}
override fun onDoubleTap(e: MotionEvent): Boolean { override fun onDoubleTap(e: MotionEvent): Boolean {
// Disables double tap gestures if view is locked // Disables double tap gestures if view is locked
if (isControlsLocked) return false if (isControlsLocked) return false
@ -100,6 +122,55 @@ class PlayerGestureHelper(
}, },
) )
@SuppressLint("SetTextI18n")
private fun enableSpeedIncrease() {
playerView.player?.let {
if (it.isPlaying) {
lastPlaybackSpeed = it.playbackParameters.speed
it.setPlaybackSpeed(playbackSpeedIncrease)
activity.binding.gestureSpeedText.text = playbackSpeedIncrease.toString() + "x"
activity.binding.gestureSpeedLayout.visibility = View.VISIBLE
}
}
}
private fun handleChapterSkip(e: MotionEvent) {
if (isControlsLocked) {
return
}
val viewWidth = playerView.measuredWidth
val areaWidth = viewWidth / 5 // Divide the view into 5 parts: 2:1:2
// Define the areas and their boundaries
val leftmostAreaStart = 0
val middleAreaStart = areaWidth * 2
val rightmostAreaStart = middleAreaStart + areaWidth
when (e.x.toInt()) {
in leftmostAreaStart until middleAreaStart -> {
activity.viewModel.seekToPreviousChapter()?.let { chapter ->
displayChapter(chapter)
}
}
in rightmostAreaStart until viewWidth -> {
if (activity.viewModel.isLastChapter() == true) {
playerView.player?.seekToNextMediaItem()
return
}
activity.viewModel.seekToNextChapter()?.let { chapter ->
displayChapter(chapter)
}
}
else -> return
}
}
private fun displayChapter(chapter: PlayerChapter) {
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
activity.binding.progressScrubberText.text = chapter.name ?: ""
}
private fun fastForward() { private fun fastForward() {
val currentPosition = playerView.player?.currentPosition ?: 0 val currentPosition = playerView.player?.currentPosition ?: 0
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
@ -315,8 +386,8 @@ class PlayerGestureHelper(
lastScaleEvent = SystemClock.elapsedRealtime() lastScaleEvent = SystemClock.elapsedRealtime()
val scaleFactor = detector.scaleFactor val scaleFactor = detector.scaleFactor
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) { if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
isZoomEnabled = scaleFactor > 1 val enableZoom = scaleFactor > 1
updateZoomMode(isZoomEnabled) updateZoomMode(enableZoom)
} }
return true return true
} }
@ -325,16 +396,17 @@ class PlayerGestureHelper(
}, },
).apply { isQuickScaleEnabled = false } ).apply { isQuickScaleEnabled = false }
private fun updateZoomMode(enabled: Boolean) { fun updateZoomMode(enabled: Boolean) {
if (playerView.player is MPVPlayer) { if (playerView.player is MPVPlayer) {
(playerView.player as MPVPlayer).updateZoomMode(enabled) (playerView.player as MPVPlayer).updateZoomMode(enabled)
} else { } else {
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
} }
isZoomEnabled = enabled
} }
private fun releaseAction(event: MotionEvent) { private fun releaseAction(event: MotionEvent) {
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
activity.binding.gestureVolumeLayout.apply { activity.binding.gestureVolumeLayout.apply {
if (visibility == View.VISIBLE) { if (visibility == View.VISIBLE) {
removeCallbacks(hideGestureVolumeIndicatorOverlayAction) removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
@ -361,6 +433,12 @@ class PlayerGestureHelper(
swipeGestureValueTrackerProgress = -1L swipeGestureValueTrackerProgress = -1L
} }
} }
currentNumberOfPointers = 0
}
if (lastPlaybackSpeed > 0 && (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL)) {
playerView.player?.setPlaybackSpeed(lastPlaybackSpeed)
lastPlaybackSpeed = 0f
activity.binding.gestureSpeedLayout.visibility = View.GONE
} }
} }
@ -398,9 +476,12 @@ class PlayerGestureHelper(
activity.window.attributes.screenBrightness = appPreferences.playerBrightness activity.window.attributes.screenBrightness = appPreferences.playerBrightness
} }
updateZoomMode(appPreferences.playerStartMaximized)
@Suppress("ClickableViewAccessibility") @Suppress("ClickableViewAccessibility")
playerView.setOnTouchListener { _, event -> playerView.setOnTouchListener { _, event ->
if (playerView.useController) { if (playerView.useController) {
currentNumberOfPointers = event.pointerCount
when (event.pointerCount) { when (event.pointerCount) {
1 -> { 1 -> {
tapGestureDetector.onTouchEvent(event) tapGestureDetector.onTouchEvent(event)

View file

@ -8,8 +8,8 @@ import androidx.media3.common.Player
import androidx.media3.ui.TimeBar import androidx.media3.ui.TimeBar
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import dev.jdtech.jellyfin.utils.bif.BifData import dev.jdtech.jellyfin.models.Trickplay
import dev.jdtech.jellyfin.utils.bif.BifUtil import kotlinx.coroutines.Dispatchers
import timber.log.Timber import timber.log.Timber
class PreviewScrubListener( class PreviewScrubListener(
@ -17,14 +17,14 @@ class PreviewScrubListener(
private val timeBarView: View, private val timeBarView: View,
private val player: Player, private val player: Player,
) : TimeBar.OnScrubListener { ) : TimeBar.OnScrubListener {
var currentTrickPlay: BifData? = null var currentTrickplay: Trickplay? = null
private val roundedCorners = RoundedCornersTransformation(10f) private val roundedCorners = RoundedCornersTransformation(10f)
private var currentBitMap: Bitmap? = null private var currentBitMap: Bitmap? = null
override fun onScrubStart(timeBar: TimeBar, position: Long) { override fun onScrubStart(timeBar: TimeBar, position: Long) {
Timber.d("Scrubbing started at $position") Timber.d("Scrubbing started at $position")
if (currentTrickPlay == null) { if (currentTrickplay == null) {
return return
} }
@ -35,8 +35,8 @@ class PreviewScrubListener(
override fun onScrubMove(timeBar: TimeBar, position: Long) { override fun onScrubMove(timeBar: TimeBar, position: Long) {
Timber.d("Scrubbing to $position") Timber.d("Scrubbing to $position")
val currentBifData = currentTrickPlay ?: return val trickplay = currentTrickplay ?: return
val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return val image = trickplay.images[position.div(trickplay.interval).toInt()]
val parent = scrubbingPreview.parent as ViewGroup val parent = scrubbingPreview.parent as ViewGroup
@ -57,6 +57,7 @@ class PreviewScrubListener(
if (currentBitMap != image) { if (currentBitMap != image) {
scrubbingPreview.load(image) { scrubbingPreview.load(image) {
dispatcher(Dispatchers.Main.immediate)
transformations(roundedCorners) transformations(roundedCorners)
} }
currentBitMap = image currentBitMap = image

View file

@ -113,6 +113,37 @@
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/gesture_speed_layout"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_gravity="center_horizontal|top"
android:layout_margin="16dp"
android:background="@drawable/overlay_background"
android:clickable="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/gesture_speed_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:gravity="center"
android:textSize="20sp"
android:textColor="@android:color/white" />
<ImageView
android:id="@+id/gesture_speed_image"
android:layout_width="36dp"
android:layout_height="24dp"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_speed_forward"
tools:ignore="ContentDescription" />
</LinearLayout>
<ImageView <ImageView
android:id="@+id/image_ffwd_animation_ripple" android:id="@+id/image_ffwd_animation_ripple"
android:layout_width="50dp" android:layout_width="50dp"

View file

@ -8,43 +8,6 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/extra_buttons_alt"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/back_button_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_corner"
android:contentDescription="@string/player_controls_exit"
android:padding="16dp"
android:src="@drawable/ic_arrow_left" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_buttons_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton <ImageButton
android:id="@+id/btn_unlock" android:id="@+id/btn_unlock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -53,10 +16,8 @@
android:background="@drawable/rounded_corner" android:background="@drawable/rounded_corner"
android:contentDescription="@string/select_playback_speed" android:contentDescription="@string/select_playback_speed"
android:padding="16dp" android:padding="16dp"
android:layout_margin="8dp"
android:src="@drawable/ic_unlock" android:src="@drawable/ic_unlock"
app:tint="@android:color/white" /> app:tint="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </FrameLayout>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/exo_controller">
<androidx.media3.ui.AspectRatioFrameLayout <androidx.media3.ui.AspectRatioFrameLayout
android:id="@id/exo_content_frame" android:id="@id/exo_content_frame"
@ -82,9 +83,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<View <androidx.media3.ui.PlayerControlView
android:id="@id/exo_controller_placeholder" android:id="@id/exo_controller"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
app:animation_enabled="false"/>
</merge> </merge>

View file

@ -141,6 +141,15 @@
android:visibility="invisible" /> android:visibility="invisible" />
</RelativeLayout> </RelativeLayout>
<TextView
android:id="@+id/login_disclaimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_margin="24dp"
android:textSize="16sp"
tools:text="Sample login disclaimer" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -123,6 +123,9 @@
<argument <argument
android:name="libraryType" android:name="libraryType"
app:argType="dev.jdtech.jellyfin.models.CollectionType" /> app:argType="dev.jdtech.jellyfin.models.CollectionType" />
<action
android:id="@+id/action_libraryFragment_self"
app:destination="@id/libraryFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/showFragment" android:id="@+id/showFragment"

View file

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
@ -56,6 +57,8 @@ android {
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = Versions.java sourceCompatibility = Versions.java
targetCompatibility = Versions.java targetCompatibility = Versions.java
} }
@ -64,10 +67,6 @@ android {
compose = true compose = true
} }
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -82,22 +81,26 @@ ktlint {
} }
dependencies { dependencies {
implementation(project(":core")) val composeBom = platform(libs.androidx.compose.bom)
implementation(project(":data"))
implementation(project(":preferences")) implementation(projects.core)
implementation(project(":player:core")) implementation(projects.data)
implementation(project(":player:video")) implementation(projects.preferences)
implementation(projects.player.core)
implementation(projects.player.video)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(composeBom)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.session)
implementation(libs.androidx.paging.compose) implementation(libs.androidx.paging.compose)
implementation(libs.androidx.tv.foundation)
implementation(libs.androidx.tv.material)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.svg) implementation(libs.coil.svg)
implementation(libs.compose.destinations.core) implementation(libs.compose.destinations.core)
@ -105,8 +108,9 @@ dependencies {
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation(libs.jellyfin.core) implementation(libs.jellyfin.core)
implementation(libs.androidx.tv.foundation) implementation(libs.media3.ffmpeg.decoder)
implementation(libs.androidx.tv.material)
coreLibraryDesugaring(libs.android.desugar.jdk)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
} }

View file

@ -28,3 +28,5 @@
-dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE -dontwarn org.openjsse.net.ssl.OpenJSSE
-keep class dev.jdtech.**

View file

@ -23,7 +23,6 @@ data class PlayerActivityNavArgs(
@ActivityDestination( @ActivityDestination(
navArgsDelegate = PlayerActivityNavArgs::class, navArgsDelegate = PlayerActivityNavArgs::class,
) )
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class PlayerActivity : ComponentActivity() { class PlayerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -71,7 +70,6 @@ fun AddServerScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun AddServerScreenLayout( private fun AddServerScreenLayout(
uiState: AddServerViewModel.UiState, uiState: AddServerViewModel.UiState,

View file

@ -22,7 +22,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -88,7 +87,6 @@ fun HomeScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun HomeScreenLayout( private fun HomeScreenLayout(
uiState: HomeViewModel.UiState, uiState: HomeViewModel.UiState,

View file

@ -17,7 +17,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.grid.TvGridCells import androidx.tv.foundation.lazy.grid.TvGridCells
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
import androidx.tv.foundation.lazy.grid.items import androidx.tv.foundation.lazy.grid.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@ -50,7 +49,6 @@ fun LibrariesScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun LibrariesScreenLayout( private fun LibrariesScreenLayout(
uiState: MediaViewModel.UiState, uiState: MediaViewModel.UiState,

View file

@ -18,14 +18,15 @@ import androidx.paging.compose.collectAsLazyPagingItems
import androidx.tv.foundation.lazy.grid.TvGridCells import androidx.tv.foundation.lazy.grid.TvGridCells
import androidx.tv.foundation.lazy.grid.TvGridItemSpan import androidx.tv.foundation.lazy.grid.TvGridItemSpan
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import dev.jdtech.jellyfin.destinations.LibraryScreenDestination
import dev.jdtech.jellyfin.destinations.MovieScreenDestination import dev.jdtech.jellyfin.destinations.MovieScreenDestination
import dev.jdtech.jellyfin.destinations.ShowScreenDestination import dev.jdtech.jellyfin.destinations.ShowScreenDestination
import dev.jdtech.jellyfin.models.CollectionType import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.FindroidFolder
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
@ -65,12 +66,14 @@ fun LibraryScreen(
is FindroidShow -> { is FindroidShow -> {
navigator.navigate(ShowScreenDestination(item.id)) navigator.navigate(ShowScreenDestination(item.id))
} }
is FindroidFolder -> {
navigator.navigate(LibraryScreenDestination(item.id, item.name, libraryType))
}
} }
}, },
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun LibraryScreenLayout( private fun LibraryScreenLayout(
libraryName: String, libraryName: String,

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -32,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -86,7 +86,6 @@ fun LoginScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun LoginScreenLayout( private fun LoginScreenLayout(
uiState: LoginViewModel.UiState, uiState: LoginViewModel.UiState,
@ -110,6 +109,14 @@ private fun LoginScreenLayout(
else -> Unit else -> Unit
} }
var disclaimer: String? by remember {
mutableStateOf(null)
}
if (uiState is LoginViewModel.UiState.Normal) {
disclaimer = uiState.disclaimer
}
val isError = uiState is LoginViewModel.UiState.Error val isError = uiState is LoginViewModel.UiState.Error
val isLoading = uiState is LoginViewModel.UiState.Loading val isLoading = uiState is LoginViewModel.UiState.Loading
@ -241,6 +248,10 @@ private fun LoginScreenLayout(
} }
} }
} }
Text(
text = disclaimer ?: "",
modifier = Modifier.padding(MaterialTheme.spacings.default),
)
} }
} }
@ -254,7 +265,7 @@ private fun LoginScreenLayout(
private fun LoginScreenLayoutPreview() { private fun LoginScreenLayoutPreview() {
FindroidTheme { FindroidTheme {
LoginScreenLayout( LoginScreenLayout(
uiState = LoginViewModel.UiState.Normal, uiState = LoginViewModel.UiState.Normal(),
quickConnectUiState = LoginViewModel.QuickConnectUiState.Normal, quickConnectUiState = LoginViewModel.QuickConnectUiState.Normal,
onLoginClick = { _, _ -> }, onLoginClick = { _, _ -> },
onQuickConnectClick = {}, onQuickConnectClick = {},

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab import androidx.tv.material3.Tab
@ -78,7 +77,6 @@ enum class TabDestination(
// LiveTV(CoreR.drawable.ic_tv, CoreR.string.live_tv) // LiveTV(CoreR.drawable.ic_tv, CoreR.string.live_tv)
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun MainScreenLayout( private fun MainScreenLayout(
uiState: MainViewModel.UiState, uiState: MainViewModel.UiState,

View file

@ -39,7 +39,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize import androidx.compose.ui.unit.toSize
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -62,6 +61,7 @@ import dev.jdtech.jellyfin.viewmodels.MovieViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.PersonKind
import java.util.UUID import java.util.UUID
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@ -115,7 +115,6 @@ fun MovieScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun MovieScreenLayout( private fun MovieScreenLayout(
uiState: MovieViewModel.UiState, uiState: MovieViewModel.UiState,
@ -345,6 +344,7 @@ private fun MovieScreenLayoutPreview() {
director = BaseItemPerson( director = BaseItemPerson(
id = UUID.randomUUID(), id = UUID.randomUUID(),
name = "Robert Rodriguez", name = "Robert Rodriguez",
type = PersonKind.DIRECTOR,
), ),
writers = emptyList(), writers = emptyList(),
videoMetadata = VideoMetadata( videoMetadata = VideoMetadata(

View file

@ -30,7 +30,6 @@ import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@ -204,7 +203,6 @@ fun PlayerScreen(
} }
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerControls( fun VideoPlayerControls(
title: String, title: String,

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -76,7 +75,6 @@ fun SeasonScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun SeasonScreenLayout( private fun SeasonScreenLayout(
seriesName: String, seriesName: String,

View file

@ -35,7 +35,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.OutlinedButton import androidx.tv.material3.OutlinedButton
@ -103,7 +102,6 @@ fun ServerSelectScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun ServerSelectScreenLayout( private fun ServerSelectScreenLayout(
uiState: ServerSelectViewModel.UiState, uiState: ServerSelectViewModel.UiState,
@ -246,7 +244,6 @@ private fun ServerSelectScreenLayoutPreviewNoServers() {
} }
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun ServerComponent( private fun ServerComponent(
server: DiscoveredServer, server: DiscoveredServer,

View file

@ -19,7 +19,6 @@ import androidx.tv.foundation.lazy.grid.TvGridCells
import androidx.tv.foundation.lazy.grid.TvGridItemSpan import androidx.tv.foundation.lazy.grid.TvGridItemSpan
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
import androidx.tv.foundation.lazy.grid.items import androidx.tv.foundation.lazy.grid.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -80,7 +79,6 @@ fun SettingsScreen(
} }
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun SettingsScreenLayout( private fun SettingsScreenLayout(
uiState: SettingsViewModel.UiState, uiState: SettingsViewModel.UiState,

View file

@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -90,7 +89,6 @@ fun SettingsSubScreen(
} }
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun SettingsSubScreenLayout( private fun SettingsSubScreenLayout(
uiState: SettingsViewModel.UiState, uiState: SettingsViewModel.UiState,

View file

@ -49,7 +49,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.foundation.lazy.list.rememberTvLazyListState import androidx.tv.foundation.lazy.list.rememberTvLazyListState
import androidx.tv.material3.Button import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -126,7 +125,6 @@ fun ShowScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun ShowScreenLayout( private fun ShowScreenLayout(
uiState: ShowViewModel.UiState, uiState: ShowViewModel.UiState,

View file

@ -33,7 +33,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.OutlinedButton import androidx.tv.material3.OutlinedButton
@ -99,7 +98,6 @@ fun UserSelectScreen(
) )
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun UserSelectScreenLayout( private fun UserSelectScreenLayout(
uiState: UserSelectViewModel.UiState, uiState: UserSelectViewModel.UiState,
@ -204,7 +202,6 @@ private fun UserSelectScreenLayoutPreviewNoUsers() {
} }
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
private fun UserComponent( private fun UserComponent(
user: User, user: User,

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyEpisode
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun EpisodeCard( fun EpisodeCard(
episode: FindroidEpisode, episode: FindroidEpisode,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -33,7 +32,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyMovie
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun ItemCard( fun ItemCard(
item: FindroidItem, item: FindroidItem,

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import coil.compose.AsyncImage import coil.compose.AsyncImage
import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidEpisode
@ -17,7 +16,6 @@ enum class Direction {
HORIZONTAL, VERTICAL HORIZONTAL, VERTICAL
} }
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun ItemPoster( fun ItemPoster(
item: FindroidItem, item: FindroidItem,

View file

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.height import androidx.compose.ui.unit.height
import androidx.compose.ui.unit.width import androidx.compose.ui.unit.width
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
@ -35,7 +34,6 @@ import androidx.tv.material3.TabRow
* *
* This component is adapted from androidx.tv.material3.TabRowDefaults.PillIndicator * This component is adapted from androidx.tv.material3.TabRowDefaults.PillIndicator
*/ */
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun PillBorderIndicator( fun PillBorderIndicator(
currentTabPosition: DpRect, currentTabPosition: DpRect,

View file

@ -17,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
import coil.compose.AsyncImage import coil.compose.AsyncImage
@ -29,7 +28,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyUser
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ImageType
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun ProfileButton( fun ProfileButton(
user: User?, user: User?,

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -25,7 +24,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun ProgressBadge( fun ProgressBadge(
item: FindroidItem, item: FindroidItem,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
@ -31,7 +30,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun SettingsCategoryCard( fun SettingsCategoryCard(
preference: PreferenceCategory, preference: PreferenceCategory,

View file

@ -22,7 +22,6 @@ import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.RadioButton import androidx.tv.material3.RadioButton
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
@ -33,7 +32,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun SettingsDetailsCard( fun SettingsDetailsCard(
preference: PreferenceSelect, preference: PreferenceSelect,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun SettingsSelectCard( fun SettingsSelectCard(
preference: PreferenceSelect, preference: PreferenceSelect,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.models.PreferenceSwitch
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun SettingsSwitchCard( fun SettingsSwitchCard(
preference: PreferenceSwitch, preference: PreferenceSwitch,

View file

@ -15,12 +15,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerControlsLayout( fun VideoPlayerControlsLayout(
mediaTitle: @Composable () -> Unit, mediaTitle: @Composable () -> Unit,

View file

@ -7,11 +7,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.IconButton import androidx.tv.material3.IconButton
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerMediaButton( fun VideoPlayerMediaButton(
icon: Painter, icon: Painter,

View file

@ -4,12 +4,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerMediaTitle( fun VideoPlayerMediaTitle(
title: String, title: String,

View file

@ -21,12 +21,10 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.ui.theme.spacings import dev.jdtech.jellyfin.ui.theme.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerOverlay( fun VideoPlayerOverlay(
isPlaying: Boolean, isPlaying: Boolean,

View file

@ -25,12 +25,11 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import dev.jdtech.jellyfin.ui.theme.FindroidTheme import dev.jdtech.jellyfin.ui.theme.FindroidTheme
import dev.jdtech.jellyfin.utils.handleDPadKeyEvents import dev.jdtech.jellyfin.utils.handleDPadKeyEvents
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun VideoPlayerSeekBar( fun VideoPlayerSeekBar(
progress: Float, progress: Float,

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.IconButton import androidx.tv.material3.IconButton
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -25,7 +24,6 @@ import dev.jdtech.jellyfin.ui.theme.spacings
import kotlin.time.Duration import kotlin.time.Duration
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun VideoPlayerSeeker( fun VideoPlayerSeeker(
focusRequester: FocusRequester, focusRequester: FocusRequester,

View file

@ -24,7 +24,6 @@ import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Border import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.RadioButton import androidx.tv.material3.RadioButton
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
@ -45,7 +44,6 @@ data class VideoPlayerTrackSelectorDialogResult(
val index: Int, val index: Int,
) : Parcelable ) : Parcelable
@OptIn(ExperimentalTvMaterial3Api::class)
@Destination(style = BaseDialogStyle::class) @Destination(style = BaseDialogStyle::class)
@Composable @Composable
fun VideoPlayerTrackSelectorDialog( fun VideoPlayerTrackSelectorDialog(

View file

@ -55,6 +55,8 @@ val dummyEpisode = FindroidEpisode(
seasonId = UUID.randomUUID(), seasonId = UUID.randomUUID(),
communityRating = 9.2f, communityRating = 9.2f,
images = FindroidImages(), images = FindroidImages(),
chapters = null,
trickplayInfo = null,
) )
val dummyEpisodes = listOf( val dummyEpisodes = listOf(

View file

@ -55,6 +55,8 @@ val dummyMovie = FindroidMovie(
endDate = null, endDate = null,
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8", trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
images = FindroidImages(), images = FindroidImages(),
chapters = null,
trickplayInfo = null,
) )
val dummyMovies = listOf( val dummyMovies = listOf(

View file

@ -2,7 +2,6 @@ package dev.jdtech.jellyfin.ui.theme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.darkColorScheme as darkColorSchemeTv import androidx.tv.material3.darkColorScheme as darkColorSchemeTv
val PrimaryDark = Color(0xffa1c9ff) val PrimaryDark = Color(0xffa1c9ff)
@ -23,7 +22,6 @@ val ColorScheme = darkColorScheme(
background = Neutral1000, background = Neutral1000,
) )
@OptIn(ExperimentalTvMaterial3Api::class)
val ColorSchemeTv = darkColorSchemeTv( val ColorSchemeTv = darkColorSchemeTv(
primary = ColorScheme.primary, primary = ColorScheme.primary,
onPrimary = ColorScheme.onPrimary, onPrimary = ColorScheme.onPrimary,

View file

@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Shapes as ShapesTv import androidx.tv.material3.Shapes as ShapesTv
val shapes = Shapes( val shapes = Shapes(
@ -11,7 +10,6 @@ val shapes = Shapes(
small = RoundedCornerShape(10.dp), small = RoundedCornerShape(10.dp),
) )
@OptIn(ExperimentalTvMaterial3Api::class)
val shapesTv = ShapesTv( val shapesTv = ShapesTv(
extraSmall = shapes.extraSmall, extraSmall = shapes.extraSmall,
small = shapes.small, small = shapes.small,

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@Immutable @Immutable
@ -17,9 +16,7 @@ data class Spacings(
val extraLarge: Dp = 64.dp, val extraLarge: Dp = 64.dp,
) )
@OptIn(ExperimentalTvMaterial3Api::class)
val MaterialTheme.spacings val MaterialTheme.spacings
get() = Spacings() get() = Spacings()
@OptIn(ExperimentalTvMaterial3Api::class)
val LocalSpacings = compositionLocalOf { MaterialTheme.spacings } val LocalSpacings = compositionLocalOf { MaterialTheme.spacings }

View file

@ -10,12 +10,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.NonInteractiveSurfaceDefaults
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
import androidx.tv.material3.SurfaceDefaults
import androidx.tv.material3.MaterialTheme as MaterialThemeTv import androidx.tv.material3.MaterialTheme as MaterialThemeTv
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun FindroidTheme( fun FindroidTheme(
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
@ -34,7 +32,7 @@ fun FindroidTheme(
shapes = shapesTv, shapes = shapesTv,
content = { content = {
Surface( Surface(
colors = NonInteractiveSurfaceDefaults.colors( colors = SurfaceDefaults.colors(
containerColor = androidx.tv.material3.MaterialTheme.colorScheme.background, containerColor = androidx.tv.material3.MaterialTheme.colorScheme.background,
), ),
shape = RectangleShape, shape = RectangleShape,

View file

@ -4,7 +4,6 @@ import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Typography as TypographyTv import androidx.tv.material3.Typography as TypographyTv
val Typography = Typography( val Typography = Typography(
@ -34,7 +33,6 @@ val Typography = Typography(
), ),
) )
@OptIn(ExperimentalTvMaterial3Api::class)
val TypographyTv = TypographyTv( val TypographyTv = TypographyTv(
displayMedium = Typography.displayMedium, displayMedium = Typography.displayMedium,
headlineMedium = Typography.headlineMedium, headlineMedium = Typography.headlineMedium,

View file

@ -1,6 +1,3 @@
import com.android.build.api.dsl.CommonExtension
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
@ -19,17 +16,6 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
} }
val configureAndroid = { _: AppliedPlugin ->
extensions.configure<CommonExtension<*, *, *, *, *>>("android") {
lint {
informational += "MissingTranslation"
}
}
}
pluginManager.withPlugin("com.android.library", configureAndroid)
pluginManager.withPlugin("com.android.application", configureAndroid)
} }
tasks.create<Delete>("clean") { tasks.create<Delete>("clean") {

View file

@ -0,0 +1 @@
rootProject.name = "buildSrc"

View file

@ -1,8 +1,8 @@
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
object Versions { object Versions {
const val appCode = 22 const val appCode = 25
const val appName = "0.13.1" const val appName = "0.14.2"
const val compileSdk = 34 const val compileSdk = 34
const val buildTools = "34.0.0" const val buildTools = "34.0.0"
@ -11,6 +11,5 @@ object Versions {
val java = JavaVersion.VERSION_17 val java = JavaVersion.VERSION_17
const val composeCompiler = "1.5.7"
const val ktlint = "0.50.0" const val ktlint = "0.50.0"
} }

View file

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.androidx.navigation.safeargs) alias(libs.plugins.androidx.navigation.safeargs)
@ -29,7 +30,6 @@ android {
flavorDimensions += "variant" flavorDimensions += "variant"
productFlavors { productFlavors {
register("libre") register("libre")
register("huawei")
} }
compileOptions { compileOptions {
@ -40,10 +40,6 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
} }
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
} }
ktlint { ktlint {
@ -53,18 +49,18 @@ ktlint {
} }
dependencies { dependencies {
implementation(project(":data")) val composeBom = platform(libs.androidx.compose.bom)
implementation(project(":preferences"))
implementation(project(":player:core")) implementation(projects.data)
implementation(libs.androidx.activity) implementation(projects.preferences)
implementation(projects.player.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(composeBom)
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.paging) implementation(libs.androidx.paging)
implementation(libs.androidx.preference) implementation(libs.androidx.preference)
implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.runtime)

View file

@ -1,81 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="1536dp"
android:height="512dp"
android:viewportWidth="1536"
android:viewportHeight="512">
<group android:scaleX="0.62"
android:scaleY="0.62"
android:translateX="-60"
android:translateY="-60">
<path
android:pathData="m512,136.5c-99.4,0 -419.3,580 -370.5,678 48.7,98 692.8,96.8 741.1,0 48.3,-96.8 -271.1,-678 -370.5,-678zM512,285.3c65,0 274.5,380.7 242.9,444.3 -31.6,63.4 -453.6,64.2 -485.6,0C237.4,665.3 447,285.3 512,285.3ZM437.6,503.2c18.3,51.8 26.5,80.7 2.8,129.3 -5.5,11.4 -0.7,20.2 11.9,20.2l131.1,0.9c12.5,0.1 19,-9.3 13.7,-20.6 -19.3,-41.2 -69.6,-130.3 -149.6,-142.7 -8.2,-1.3 -12.8,5 -10,12.9z"
android:strokeWidth="0.23938">
<aapt:attr name="android:fillColor">
<gradient
android:startY="479.77658"
android:startX="363.41766"
android:endY="702.5666"
android:endX="749.3077"
android:type="linear">
<item android:offset="0" android:color="@color/logo_primary"/>
<item android:offset="1" android:color="@color/logo_secondary"/>
</gradient>
</aapt:attr>
</path>
</group>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m569.69,357.11q-3.73,0 -6.13,-2.4 -2.13,-2.4 -2.13,-5.6v-170.67q0,-3.2 2.4,-5.6t5.6,-2.4h97.07q3.47,0 5.6,2.4 2.4,2.13 2.4,5.6 0,3.2 -2.4,5.6 -2.13,2.13 -5.6,2.13h-89.87l1.07,-1.6v70.93l-1.33,-2.4h78.13q3.47,0 5.6,2.4 2.4,2.13 2.4,5.33 0,3.47 -2.4,5.6 -2.13,2.13 -5.6,2.13h-78.67l1.87,-2.13v82.67q0,3.2 -2.4,5.6 -2.13,2.4 -5.6,2.4z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m723.83,349.11q0,3.2 -2.4,5.6t-5.6,2.4q-3.47,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-122.67q0,-3.2 2.13,-5.6 2.4,-2.4 5.87,-2.4t5.6,2.4q2.4,2.4 2.4,5.6zM715.83,200.58q-5.6,0 -8.53,-2.4 -2.67,-2.67 -2.67,-7.47v-2.67q0,-4.8 2.93,-7.2 3.2,-2.67 8.53,-2.67 5.07,0 7.73,2.67 2.93,2.4 2.93,7.2v2.67q0,4.8 -2.93,7.47 -2.67,2.4 -8,2.4z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m829.69,217.65q17.6,0 28,7.2 10.67,6.93 15.2,19.2 4.8,12 4.8,26.67v78.4q0,3.2 -2.4,5.6t-5.6,2.4q-3.73,0 -5.87,-2.4t-2.13,-5.6v-77.6q0,-10.67 -3.47,-19.47t-11.47,-14.13q-7.73,-5.33 -20.53,-5.33 -11.47,0 -21.87,5.33 -10.13,5.33 -16.53,14.13 -6.4,8.8 -6.4,19.47v77.6q0,3.2 -2.4,5.6 -2.4,2.4 -5.6,2.4 -3.73,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-119.47q0,-3.2 2.13,-5.6 2.4,-2.4 5.87,-2.4t5.6,2.4q2.4,2.4 2.4,5.6v22.4l-6.13,9.6q0.53,-8.53 5.33,-16.27 5.07,-8 12.8,-14.13 7.73,-6.4 17.07,-9.87 9.6,-3.73 19.2,-3.73z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1030.2,159.78q3.47,0 5.6,2.4 2.4,2.13 2.4,5.6v181.33q0,3.2 -2.4,5.6t-5.6,2.4q-3.73,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-31.73l4.53,-3.73q0,7.47 -4,15.73 -4,8 -11.47,14.93 -7.2,6.93 -17.07,11.2 -9.6,4.27 -21.07,4.27 -17.6,0 -32,-9.33 -14.13,-9.33 -22.4,-25.33 -8.27,-16 -8.27,-36.53 0,-20.27 8.27,-36.27 8.27,-16.27 22.4,-25.33 14.13,-9.33 31.73,-9.33 11.2,0 21.07,4 9.87,4 17.33,10.93 7.73,6.93 12,16 4.53,8.8 4.53,18.4l-5.6,-4v-95.2q0,-3.2 2.13,-5.6 2.13,-2.4 5.87,-2.4zM974.73,344.85q14.13,0 25.07,-7.2 10.93,-7.47 17.07,-20 6.4,-12.8 6.4,-29.07 0,-16 -6.4,-28.53 -6.13,-12.8 -17.07,-20 -10.93,-7.47 -25.07,-7.47 -13.87,0 -25.07,7.47 -10.93,7.2 -17.33,20 -6.13,12.53 -6.13,28.53t6.13,28.8q6.4,12.8 17.33,20.27 11.2,7.2 25.07,7.2z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1088.1,357.11q-3.73,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-119.47q0,-3.2 2.13,-5.6 2.4,-2.4 5.87,-2.4t5.6,2.4q2.4,2.4 2.4,5.6v40l-4,0.8q0.8,-9.33 4.53,-18.4 4,-9.33 10.67,-17.07t15.73,-12.53q9.33,-4.8 20.8,-4.8 4.8,0 9.33,2.13 4.53,1.87 4.53,6.4 0,4 -2.13,6.13 -2.13,2.13 -5.07,2.13 -2.4,0 -5.33,-1.33 -2.67,-1.33 -7.2,-1.33 -7.47,0 -14.93,4.53 -7.47,4.27 -13.6,11.73 -6.13,7.47 -9.87,16.8 -3.47,9.07 -3.47,18.4v65.87q0,3.2 -2.4,5.6t-5.6,2.4z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1302.8,288.85q0,20.27 -9.07,36.53 -8.8,16 -24,25.33 -15.2,9.07 -34.4,9.07 -18.93,0 -34.4,-9.07 -15.2,-9.33 -24.27,-25.33 -8.8,-16.27 -8.8,-36.53 0,-20.53 8.8,-36.53 9.07,-16 24.27,-25.33 15.47,-9.33 34.4,-9.33 19.2,0 34.4,9.33 15.2,9.33 24,25.33 9.07,16 9.07,36.53zM1286.8,288.85q0,-16.27 -6.67,-28.8 -6.67,-12.8 -18.4,-20 -11.47,-7.47 -26.4,-7.47 -14.67,0 -26.4,7.47 -11.47,7.2 -18.4,20 -6.67,12.53 -6.67,28.8 0,16.27 6.67,28.8 6.93,12.53 18.4,20 11.73,7.2 26.4,7.2 14.93,0 26.4,-7.2 11.73,-7.47 18.4,-20 6.67,-12.53 6.67,-28.8z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1351,349.11q0,3.2 -2.4,5.6t-5.6,2.4q-3.47,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-122.67q0,-3.2 2.13,-5.6 2.4,-2.4 5.87,-2.4 3.47,0 5.6,2.4 2.4,2.4 2.4,5.6zM1343,200.58q-5.6,0 -8.53,-2.4 -2.67,-2.67 -2.67,-7.47v-2.67q0,-4.8 2.93,-7.2 3.2,-2.67 8.53,-2.67 5.07,0 7.73,2.67 2.93,2.4 2.93,7.2v2.67q0,4.8 -2.93,7.47 -2.67,2.4 -8,2.4z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1503.3,159.78q3.47,0 5.6,2.4 2.4,2.13 2.4,5.6v181.33q0,3.2 -2.4,5.6t-5.6,2.4q-3.73,0 -5.87,-2.4 -2.13,-2.4 -2.13,-5.6v-31.73l4.53,-3.73q0,7.47 -4,15.73 -4,8 -11.47,14.93 -7.2,6.93 -17.07,11.2 -9.6,4.27 -21.07,4.27 -17.6,0 -32,-9.33 -14.13,-9.33 -22.4,-25.33 -8.27,-16 -8.27,-36.53 0,-20.27 8.27,-36.27 8.27,-16.27 22.4,-25.33 14.13,-9.33 31.73,-9.33 11.2,0 21.07,4 9.87,4 17.33,10.93 7.73,6.93 12,16 4.53,8.8 4.53,18.4l-5.6,-4v-95.2q0,-3.2 2.13,-5.6 2.13,-2.4 5.87,-2.4zM1447.83,344.85q14.13,0 25.07,-7.2 10.93,-7.47 17.07,-20 6.4,-12.8 6.4,-29.07 0,-16 -6.4,-28.53 -6.13,-12.8 -17.07,-20 -10.93,-7.47 -25.07,-7.47 -13.87,0 -25.07,7.47 -10.93,7.2 -17.33,20 -6.13,12.53 -6.13,28.53t6.13,28.8q6.4,12.8 17.33,20.27 11.2,7.2 25.07,7.2z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1093.4,453.74q-1.19,0 -1.96,-0.77 -0.68,-0.77 -0.68,-1.79v-54.61q0,-1.02 0.77,-1.79t1.79,-0.77h31.06q1.11,0 1.79,0.77 0.77,0.68 0.77,1.79 0,1.02 -0.77,1.79 -0.68,0.68 -1.79,0.68h-28.76l0.34,-0.51v22.7l-0.43,-0.77h25q1.11,0 1.79,0.77 0.77,0.68 0.77,1.71 0,1.11 -0.77,1.79 -0.68,0.68 -1.79,0.68h-25.17l0.6,-0.68v26.45q0,1.02 -0.77,1.79 -0.68,0.77 -1.79,0.77z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1176.8,431.9q0,6.49 -2.9,11.69 -2.82,5.12 -7.68,8.11 -4.86,2.9 -11.01,2.9 -6.06,0 -11.01,-2.9 -4.86,-2.99 -7.77,-8.11 -2.82,-5.21 -2.82,-11.69 0,-6.57 2.82,-11.69 2.9,-5.12 7.77,-8.11 4.95,-2.99 11.01,-2.99 6.14,0 11.01,2.99 4.86,2.99 7.68,8.11 2.9,5.12 2.9,11.69zM1171.68,431.9q0,-5.21 -2.13,-9.22 -2.13,-4.1 -5.89,-6.4 -3.67,-2.39 -8.45,-2.39 -4.69,0 -8.45,2.39 -3.67,2.3 -5.89,6.4 -2.13,4.01 -2.13,9.22 0,5.21 2.13,9.22 2.22,4.01 5.89,6.4 3.75,2.3 8.45,2.3 4.78,0 8.45,-2.3 3.75,-2.39 5.89,-6.4 2.13,-4.01 2.13,-9.22z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1189.9,453.74q-1.19,0 -1.88,-0.77 -0.68,-0.77 -0.68,-1.79v-38.23q0,-1.02 0.68,-1.79 0.77,-0.77 1.88,-0.77 1.11,0 1.79,0.77 0.77,0.77 0.77,1.79v12.8l-1.28,0.26q0.26,-2.99 1.45,-5.89 1.28,-2.99 3.41,-5.46 2.13,-2.47 5.03,-4.01 2.99,-1.54 6.66,-1.54 1.54,0 2.99,0.68 1.45,0.6 1.45,2.05 0,1.28 -0.68,1.96 -0.68,0.68 -1.62,0.68 -0.77,0 -1.71,-0.43 -0.85,-0.43 -2.3,-0.43 -2.39,0 -4.78,1.45 -2.39,1.37 -4.35,3.75 -1.96,2.39 -3.16,5.38 -1.11,2.9 -1.11,5.89v21.08q0,1.02 -0.77,1.79t-1.79,0.77z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1258.9,454.59q-5.38,0 -9.64,-2.99 -4.27,-2.99 -6.49,-7.85 -0.43,-0.77 -0.43,-1.37 0,-1.11 0.85,-1.71 0.85,-0.68 1.71,-0.68t1.37,0.43q0.6,0.43 1.02,1.02 1.62,3.58 4.69,5.8 3.07,2.22 6.91,2.22 3.93,0 6.91,-1.62 2.99,-1.71 4.61,-4.69 1.71,-2.99 1.71,-6.83v-39.76q0,-1.02 0.77,-1.79 0.85,-0.77 1.96,-0.77 1.19,0 1.88,0.77 0.77,0.77 0.77,1.79v39.76q0,5.29 -2.39,9.47 -2.39,4.1 -6.57,6.49 -4.18,2.3 -9.64,2.3z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1311.6,454.59q-6.57,0 -11.6,-2.82t-7.85,-7.85q-2.82,-5.03 -2.82,-11.78 0,-7.25 2.82,-12.37 2.9,-5.12 7.42,-7.85 4.61,-2.82 9.73,-2.82 3.75,0 7.25,1.37 3.58,1.28 6.31,3.93 2.73,2.56 4.44,6.31 1.71,3.75 1.79,8.7 0,1.02 -0.77,1.79 -0.77,0.68 -1.79,0.68h-34.22l-1.02,-4.61h33.62l-1.11,1.02v-1.71q-0.43,-4.01 -2.65,-6.83 -2.22,-2.82 -5.38,-4.27 -3.07,-1.45 -6.49,-1.45 -2.56,0 -5.29,1.02 -2.65,1.02 -4.86,3.24 -2.13,2.13 -3.5,5.55 -1.37,3.33 -1.37,7.94 0,5.03 2.05,9.13 2.05,4.1 5.89,6.49 3.84,2.39 9.3,2.39 2.9,0 5.29,-0.85 2.39,-0.85 4.18,-2.22 1.88,-1.45 3.07,-2.99 0.94,-0.77 1.79,-0.77 0.94,0 1.54,0.68 0.68,0.68 0.68,1.54 0,1.02 -0.85,1.79 -2.56,3.07 -6.66,5.38 -4.1,2.22 -8.96,2.22z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1345.4,451.18q0,1.02 -0.77,1.79t-1.79,0.77q-1.11,0 -1.88,-0.77 -0.68,-0.77 -0.68,-1.79v-58.03q0,-1.02 0.77,-1.79t1.79,-0.77q1.11,0 1.79,0.77 0.77,0.77 0.77,1.79z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1365,451.18q0,1.02 -0.77,1.79t-1.79,0.77q-1.11,0 -1.88,-0.77 -0.68,-0.77 -0.68,-1.79v-58.03q0,-1.02 0.77,-1.79t1.79,-0.77q1.11,0 1.79,0.77 0.77,0.77 0.77,1.79z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1410.2,409.37q1.11,0 1.79,0.77 0.77,0.77 0.77,1.79v37.63q0,6.91 -2.73,11.6 -2.73,4.78 -7.34,7.17 -4.61,2.47 -10.5,2.47 -3.67,0 -6.83,-0.85 -3.07,-0.77 -5.03,-2.05 -1.02,-0.6 -1.54,-1.45t-0.09,-1.79q0.43,-1.19 1.28,-1.62 0.94,-0.34 1.88,0.09 1.45,0.77 4.18,1.88 2.73,1.11 6.23,1.11 4.69,0 8.11,-1.96 3.5,-1.96 5.38,-5.72 1.88,-3.67 1.88,-8.79v-6.14l0.6,2.05q-1.28,2.65 -3.67,4.69 -2.3,2.05 -5.38,3.24 -2.99,1.11 -6.4,1.11 -5.12,0 -8.53,-2.05 -3.33,-2.13 -4.95,-5.8t-1.62,-8.62v-26.2q0,-1.02 0.68,-1.79 0.68,-0.77 1.88,-0.77 1.11,0 1.79,0.77 0.77,0.77 0.77,1.79v25.43q0,5.97 2.56,9.22 2.65,3.24 8.53,3.24 3.67,0 6.74,-1.71 3.07,-1.79 5.03,-4.61 1.96,-2.9 1.96,-6.14v-25.43q0,-1.02 0.68,-1.79 0.77,-0.77 1.88,-0.77z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1441.9,390.94q1.28,0 2.73,0.26 1.54,0.26 2.65,0.94 1.11,0.6 1.11,1.88 0,0.94 -0.68,1.71 -0.68,0.68 -1.54,0.68 -0.85,0 -2.13,-0.43 -1.28,-0.51 -2.73,-0.51 -1.79,0 -3.07,0.85 -1.28,0.77 -1.96,2.3 -0.68,1.45 -0.68,3.58v48.98q0,1.02 -0.77,1.79 -0.68,0.77 -1.79,0.77t-1.88,-0.77q-0.68,-0.77 -0.68,-1.79v-48.98q0,-5.46 3.16,-8.36 3.24,-2.9 8.28,-2.9zM1445.14,410.99q1.02,0 1.71,0.68 0.68,0.68 0.68,1.71t-0.68,1.71q-0.68,0.68 -1.71,0.68h-20.91q-0.94,0 -1.71,-0.68 -0.68,-0.77 -0.68,-1.71 0,-1.11 0.68,-1.71 0.77,-0.68 1.71,-0.68zM1461.19,451.18q0,1.02 -0.77,1.79t-1.79,0.77q-1.11,0 -1.88,-0.77 -0.68,-0.77 -0.68,-1.79v-39.25q0,-1.02 0.68,-1.79 0.77,-0.77 1.88,-0.77t1.79,0.77q0.77,0.77 0.77,1.79zM1458.63,403.65q-1.79,0 -2.73,-0.77 -0.85,-0.85 -0.85,-2.39v-0.85q0,-1.54 0.94,-2.3 1.02,-0.85 2.73,-0.85 1.62,0 2.47,0.85 0.94,0.77 0.94,2.3v0.85q0,1.54 -0.94,2.39 -0.85,0.77 -2.56,0.77z"/>
<path
android:fillColor="?attr/colorOnBackground"
android:pathData="m1495.1,409.11q5.63,0 8.96,2.3 3.41,2.22 4.86,6.14 1.54,3.84 1.54,8.53v25.09q0,1.02 -0.77,1.79 -0.77,0.77 -1.79,0.77 -1.19,0 -1.88,-0.77 -0.68,-0.77 -0.68,-1.79v-24.83q0,-3.41 -1.11,-6.23 -1.11,-2.82 -3.67,-4.52 -2.47,-1.71 -6.57,-1.71 -3.67,0 -7,1.71 -3.24,1.71 -5.29,4.52t-2.05,6.23v24.83q0,1.02 -0.77,1.79t-1.79,0.77q-1.19,0 -1.88,-0.77t-0.68,-1.79v-38.23q0,-1.02 0.68,-1.79 0.77,-0.77 1.88,-0.77 1.11,0 1.79,0.77 0.77,0.77 0.77,1.79v7.17l-1.96,3.07q0.17,-2.73 1.71,-5.21 1.62,-2.56 4.1,-4.52 2.47,-2.05 5.46,-3.16 3.07,-1.19 6.14,-1.19z"/>
</vector>

View file

@ -1,60 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="320dp"
android:height="180dp"
android:viewportWidth="320"
android:viewportHeight="180">
<group android:scaleX="0.6666667"
android:scaleY="0.6666667"
android:translateX="53.333332"
android:translateY="30">
<group android:scaleX="1.6666666"
android:scaleY="1.6666666"
android:translateX="-16.4">
<group android:scaleX="0.056557618"
android:scaleY="0.056557618"
android:translateX="25.0425"
android:translateY="25.0425">
<path
android:pathData="m512,136.5c-99.4,0 -419.3,580 -370.5,678 48.7,98 692.8,96.8 741.1,0 48.3,-96.8 -271.1,-678 -370.5,-678zM512,285.3c65,0 274.5,380.7 242.9,444.3 -31.6,63.4 -453.6,64.2 -485.6,0C237.4,665.3 447,285.3 512,285.3ZM437.6,503.2c18.3,51.8 26.5,80.7 2.8,129.3 -5.5,11.4 -0.7,20.2 11.9,20.2l131.1,0.9c12.5,0.1 19,-9.3 13.7,-20.6 -19.3,-41.2 -69.6,-130.3 -149.6,-142.7 -8.2,-1.3 -12.8,5 -10,12.9z"
android:strokeWidth="0.23938">
<aapt:attr name="android:fillColor">
<gradient
android:startY="479.77658"
android:startX="363.41766"
android:endY="702.5666"
android:endX="749.3077"
android:type="linear">
<item android:offset="0" android:color="@color/logo_primary"/>
<item android:offset="1" android:color="@color/logo_secondary"/>
</gradient>
</aapt:attr>
</path>
</group>
</group>
<group android:scaleX="0.29483145"
android:scaleY="0.29483145"
android:translateX="128"
android:translateY="63.46516">
<group android:translateY="144.00006">
<path android:pathData="M17.28125,0.5Q15.984375,0.5,15.1875,-0.359375Q14.40625,-1.21875,14.40625,-2.375L14.40625,-97.125Q14.40625,-98.265625,15.265625,-99.125Q16.125,-100,17.28125,-100L71.5625,-100Q72.71875,-100,73.578125,-99.125Q74.453125,-98.265625,74.453125,-97.125Q74.453125,-95.828125,73.578125,-95.03125Q72.71875,-94.25,71.5625,-94.25L19.734375,-94.25L20.15625,-94.828125L20.15625,-53.59375L19.578125,-54.734375L65.09375,-54.734375Q66.234375,-54.734375,67.09375,-53.875Q67.96875,-53.015625,67.96875,-51.875Q67.96875,-50.578125,67.09375,-49.78125Q66.234375,-49,65.09375,-49L19.296875,-49L20.15625,-50.140625L20.15625,-2.375Q20.15625,-1.21875,19.359375,-0.359375Q18.578125,0.5,17.28125,0.5Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M99.71875,-2.875Q99.71875,-1.71875,98.84375,-0.859375Q97.984375,0,96.828125,0Q95.53125,0,94.734375,-0.859375Q93.953125,-1.71875,93.953125,-2.875L93.953125,-71.125Q93.953125,-72.28125,94.8125,-73.140625Q95.6875,-74,96.828125,-74Q98.125,-74,98.921875,-73.140625Q99.71875,-72.28125,99.71875,-71.125L99.71875,-2.875ZM96.828125,-83.65625Q94.53125,-83.65625,93.15625,-84.9375Q91.796875,-86.234375,91.796875,-88.25L91.796875,-89.390625Q91.796875,-91.40625,93.234375,-92.703125Q94.671875,-94,96.96875,-94Q98.984375,-94,100.359375,-92.703125Q101.734375,-91.40625,101.734375,-89.390625L101.734375,-88.25Q101.734375,-86.234375,100.359375,-84.9375Q98.984375,-83.65625,96.828125,-83.65625Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M156.64062,-74Q165.71875,-74,171.40625,-70.40625Q177.09375,-66.8125,179.75,-60.640625Q182.42188,-54.484375,182.42188,-46.859375L182.42188,-2.921875Q182.42188,-1.765625,181.54688,-0.90625Q180.6875,-0.046875,179.53125,-0.046875Q178.23438,-0.046875,177.4375,-0.90625Q176.65625,-1.765625,176.65625,-2.921875L176.65625,-46.28125Q176.65625,-52.46875,174.5625,-57.484375Q172.48438,-62.515625,167.9375,-65.53125Q163.40625,-68.546875,156.0625,-68.546875Q149.73438,-68.546875,143.67188,-65.53125Q137.625,-62.515625,133.67188,-57.484375Q129.71875,-52.46875,129.71875,-46.28125L129.71875,-2.921875Q129.71875,-1.765625,128.84375,-0.90625Q127.984375,-0.046875,126.828125,-0.046875Q125.53125,-0.046875,124.734375,-0.90625Q123.953125,-1.765625,123.953125,-2.921875L123.953125,-68.828125Q123.953125,-69.984375,124.8125,-70.84375Q125.6875,-71.703125,126.828125,-71.703125Q128.125,-71.703125,128.92188,-70.84375Q129.71875,-69.984375,129.71875,-68.828125L129.71875,-54.765625L126.109375,-47.875Q126.109375,-53.0625,128.84375,-57.796875Q131.57812,-62.53125,136.04688,-66.1875Q140.51562,-69.84375,145.90625,-71.921875Q151.3125,-74,156.64062,-74Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M264.84375,-106Q266.14062,-106,266.9375,-105.140625Q267.73438,-104.28125,267.73438,-103.15625L267.73438,-2.84375Q267.73438,-1.6875,266.85938,-0.828125Q266,0.03125,264.84375,0.03125Q263.54688,0.03125,262.75,-0.828125Q261.96875,-1.6875,261.96875,-2.84375L261.96875,-23.265625L264.26562,-26.421875Q264.26562,-21.53125,262.03125,-16.5625Q259.8125,-11.609375,255.70312,-7.515625Q251.59375,-3.421875,246.125,-0.96875Q240.65625,1.46875,234.3125,1.46875Q224.8125,1.46875,217.25,-3.484375Q209.70312,-8.453125,205.29688,-17Q200.90625,-25.5625,200.90625,-36.34375Q200.90625,-47.109375,205.29688,-55.671875Q209.70312,-64.234375,217.25,-69.109375Q224.8125,-74,234.3125,-74Q240.21875,-74,245.625,-71.765625Q251.03125,-69.546875,255.20312,-65.515625Q259.375,-61.5,261.8125,-56.09375Q264.26562,-50.703125,264.26562,-44.390625L261.96875,-47.984375L261.96875,-103.15625Q261.96875,-104.296875,262.75,-105.140625Q263.54688,-106,264.84375,-106ZM234.60938,-4Q242.8125,-4,249.07812,-8.15625Q255.34375,-12.328125,258.9375,-19.65625Q262.54688,-27,262.54688,-36.34375Q262.54688,-45.6875,258.9375,-52.9375Q255.34375,-60.203125,249,-64.375Q242.67188,-68.546875,234.60938,-68.546875Q226.6875,-68.546875,220.34375,-64.375Q214.01562,-60.203125,210.34375,-52.9375Q206.67188,-45.6875,206.67188,-36.34375Q206.67188,-27.140625,210.34375,-19.796875Q214.01562,-12.46875,220.34375,-8.234375Q226.6875,-4,234.60938,-4Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M294.82812,0.03125Q293.53125,0.03125,292.73438,-0.828125Q291.95312,-1.6875,291.95312,-2.84375L291.95312,-68.828125Q291.95312,-69.984375,292.8125,-70.84375Q293.6875,-71.703125,294.82812,-71.703125Q296.125,-71.703125,296.92188,-70.84375Q297.71875,-69.984375,297.71875,-68.828125L297.71875,-44.546875L295.26562,-40.796875Q295.26562,-46.390625,297.20312,-52.140625Q299.15625,-57.90625,302.89062,-62.859375Q306.64062,-67.828125,312.03125,-70.90625Q317.4375,-74,324.5,-74Q326.51562,-74,328.8125,-73.421875Q331.125,-72.859375,331.125,-70.703125Q331.125,-69.40625,330.40625,-68.546875Q329.6875,-67.6875,328.53125,-67.6875Q327.65625,-67.6875,326.4375,-68.328125Q325.21875,-68.96875,322.90625,-68.96875Q318.29688,-68.96875,313.82812,-66.234375Q309.375,-63.515625,305.70312,-58.984375Q302.03125,-54.46875,299.875,-49.140625Q297.71875,-43.8125,297.71875,-38.78125L297.71875,-2.84375Q297.71875,-1.6875,296.84375,-0.828125Q295.98438,0.03125,294.82812,0.03125Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M410.90625,-36.203125Q410.90625,-25.5625,406.29688,-17Q401.6875,-8.453125,393.6875,-3.484375Q385.70312,1.46875,375.48438,1.46875Q365.40625,1.46875,357.32812,-3.484375Q349.26562,-8.453125,344.57812,-17Q339.90625,-25.5625,339.90625,-36.203125Q339.90625,-46.96875,344.57812,-55.53125Q349.26562,-64.09375,357.32812,-69.046875Q365.40625,-74,375.48438,-74Q385.70312,-74,393.6875,-69.046875Q401.6875,-64.09375,406.29688,-55.53125Q410.90625,-46.96875,410.90625,-36.203125ZM405.14062,-36.203125Q405.14062,-45.53125,401.32812,-52.796875Q397.51562,-60.0625,390.8125,-64.296875Q384.125,-68.546875,375.48438,-68.546875Q366.98438,-68.546875,360.21875,-64.296875Q353.45312,-60.0625,349.5625,-52.796875Q345.67188,-45.53125,345.67188,-36.203125Q345.67188,-27,349.5625,-19.734375Q353.45312,-12.46875,360.21875,-8.234375Q366.98438,-4,375.48438,-4Q384.125,-4,390.8125,-8.234375Q397.51562,-12.46875,401.32812,-19.734375Q405.14062,-27,405.14062,-36.203125Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M435.71875,-2.875Q435.71875,-1.71875,434.84375,-0.859375Q433.98438,0,432.82812,0Q431.53125,0,430.73438,-0.859375Q429.95312,-1.71875,429.95312,-2.875L429.95312,-71.125Q429.95312,-72.28125,430.8125,-73.140625Q431.6875,-74,432.82812,-74Q434.125,-74,434.92188,-73.140625Q435.71875,-72.28125,435.71875,-71.125L435.71875,-2.875ZM432.82812,-83.65625Q430.53125,-83.65625,429.15625,-84.9375Q427.79688,-86.234375,427.79688,-88.25L427.79688,-89.390625Q427.79688,-91.40625,429.23438,-92.703125Q430.67188,-94,432.96875,-94Q434.98438,-94,436.35938,-92.703125Q437.73438,-91.40625,437.73438,-89.390625L437.73438,-88.25Q437.73438,-86.234375,436.35938,-84.9375Q434.98438,-83.65625,432.82812,-83.65625Z"
android:fillColor="#FFFFFF"/>
<path android:pathData="M518.84375,-106Q520.1406,-106,520.9375,-105.140625Q521.7344,-104.28125,521.7344,-103.15625L521.7344,-2.84375Q521.7344,-1.6875,520.8594,-0.828125Q520,0.03125,518.84375,0.03125Q517.5469,0.03125,516.75,-0.828125Q515.96875,-1.6875,515.96875,-2.84375L515.96875,-23.265625L518.2656,-26.421875Q518.2656,-21.53125,516.03125,-16.5625Q513.8125,-11.609375,509.70312,-7.515625Q505.59375,-3.421875,500.125,-0.96875Q494.65625,1.46875,488.3125,1.46875Q478.8125,1.46875,471.25,-3.484375Q463.70312,-8.453125,459.29688,-17Q454.90625,-25.5625,454.90625,-36.34375Q454.90625,-47.109375,459.29688,-55.671875Q463.70312,-64.234375,471.25,-69.109375Q478.8125,-74,488.3125,-74Q494.21875,-74,499.625,-71.765625Q505.03125,-69.546875,509.20312,-65.515625Q513.375,-61.5,515.8125,-56.09375Q518.2656,-50.703125,518.2656,-44.390625L515.96875,-47.984375L515.96875,-103.15625Q515.96875,-104.296875,516.75,-105.140625Q517.5469,-106,518.84375,-106ZM488.60938,-4Q496.8125,-4,503.07812,-8.15625Q509.34375,-12.328125,512.9375,-19.65625Q516.5469,-27,516.5469,-36.34375Q516.5469,-45.6875,512.9375,-52.9375Q509.34375,-60.203125,503,-64.375Q496.67188,-68.546875,488.60938,-68.546875Q480.6875,-68.546875,474.34375,-64.375Q468.01562,-60.203125,464.34375,-52.9375Q460.67188,-45.6875,460.67188,-36.34375Q460.67188,-27.140625,464.34375,-19.796875Q468.01562,-12.46875,474.34375,-8.234375Q480.6875,-4,488.60938,-4Z"
android:fillColor="#FFFFFF"/>
</group>
</group>
</group>
</vector>

View file

@ -1,27 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.056557618"
android:scaleY="0.056557618"
android:translateX="25.0425"
android:translateY="25.0425">
<path
android:pathData="m512,136.5c-99.4,0 -419.3,580 -370.5,678 48.7,98 692.8,96.8 741.1,0 48.3,-96.8 -271.1,-678 -370.5,-678zM512,285.3c65,0 274.5,380.7 242.9,444.3 -31.6,63.4 -453.6,64.2 -485.6,0C237.4,665.3 447,285.3 512,285.3ZM437.6,503.2c18.3,51.8 26.5,80.7 2.8,129.3 -5.5,11.4 -0.7,20.2 11.9,20.2l131.1,0.9c12.5,0.1 19,-9.3 13.7,-20.6 -19.3,-41.2 -69.6,-130.3 -149.6,-142.7 -8.2,-1.3 -12.8,5 -10,12.9z"
android:strokeWidth="0.23938">
<aapt:attr name="android:fillColor">
<gradient
android:startY="479.77658"
android:startX="363.41766"
android:endY="702.5666"
android:endX="749.3077"
android:type="linear">
<item android:offset="0" android:color="@color/logo_primary"/>
<item android:offset="1" android:color="@color/logo_secondary"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View file

@ -35,8 +35,10 @@ object ApiModule {
val user = serverWithAddressAndUser.user val user = serverWithAddressAndUser.user
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user?.accessToken baseUrl = serverAddress.address,
accessToken = user?.accessToken,
)
userId = user?.id userId = user?.id
} }

View file

@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.dialogs
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.widget.EditText import android.widget.EditText
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -15,13 +16,14 @@ class AddServerAddressDialog(
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val editText = EditText(this.context) val editText = EditText(this.context)
editText.hint = "http://<server_ip>:8096" editText.hint = "http://<server_ip>:8096"
editText.inputType = InputType.TYPE_TEXT_VARIATION_URI
return activity?.let { activity -> return activity?.let { activity ->
val builder = MaterialAlertDialogBuilder(activity) val builder = MaterialAlertDialogBuilder(activity)
builder builder
.setTitle(getString(R.string.add_server_address)) .setTitle(getString(R.string.add_server_address))
.setView(editText) .setView(editText)
.setPositiveButton(getString(R.string.add)) { _, _ -> .setPositiveButton(getString(R.string.add)) { _, _ ->
viewModel.addAddress(editText.text.toString()) viewModel.addAddress(requireContext(), editText.text.toString())
} }
.setNegativeButton(getString(R.string.cancel)) { _, _ -> .setNegativeButton(getString(R.string.cancel)) { _, _ ->
} }

View file

@ -44,7 +44,7 @@ class SortDialogFragment(
when (sortType) { when (sortType) {
"sortBy" -> { "sortBy" -> {
val sortByOptions = resources.getStringArray(R.array.sort_by_options) val sortByOptions = resources.getStringArray(R.array.sort_by_options)
val sortByValues = SortBy.values() val sortByValues = SortBy.entries
builder builder
.setTitle(getString(R.string.sort_by)) .setTitle(getString(R.string.sort_by))
.setSingleChoiceItems( .setSingleChoiceItems(
@ -64,7 +64,7 @@ class SortDialogFragment(
} }
"sortOrder" -> { "sortOrder" -> {
val sortByOptions = resources.getStringArray(R.array.sort_order_options) val sortByOptions = resources.getStringArray(R.array.sort_order_options)
val sortOrderValues = SortOrder.values() val sortOrderValues = SortOrder.entries
builder builder
.setTitle(getString(R.string.sort_order)) .setTitle(getString(R.string.sort_order))

View file

@ -18,7 +18,7 @@ fun BaseItemDto.toView(): View {
return View( return View(
id = id, id = id,
name = name ?: "", name = name ?: "",
type = CollectionType.fromString(collectionType), type = CollectionType.fromString(collectionType?.serialName),
) )
} }

View file

@ -13,7 +13,8 @@ import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.FindroidSources
import dev.jdtech.jellyfin.models.FindroidTrickplayInfo
import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
@ -21,13 +22,14 @@ import dev.jdtech.jellyfin.models.toFindroidMovieDto
import dev.jdtech.jellyfin.models.toFindroidSeasonDto import dev.jdtech.jellyfin.models.toFindroidSeasonDto
import dev.jdtech.jellyfin.models.toFindroidShowDto import dev.jdtech.jellyfin.models.toFindroidShowDto
import dev.jdtech.jellyfin.models.toFindroidSourceDto import dev.jdtech.jellyfin.models.toFindroidSourceDto
import dev.jdtech.jellyfin.models.toFindroidTrickplayInfoDto
import dev.jdtech.jellyfin.models.toFindroidUserDataDto import dev.jdtech.jellyfin.models.toFindroidUserDataDto
import dev.jdtech.jellyfin.models.toIntroDto import dev.jdtech.jellyfin.models.toIntroDto
import dev.jdtech.jellyfin.models.toTrickPlayManifestDto
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import kotlin.Exception import kotlin.Exception
import kotlin.math.ceil
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
class DownloaderImpl( class DownloaderImpl(
@ -46,12 +48,8 @@ class DownloaderImpl(
try { try {
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
val intro = jellyfinRepository.getIntroTimestamps(item.id) val intro = jellyfinRepository.getIntroTimestamps(item.id)
val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id) val trickplayInfo = if (item is FindroidSources) {
val trickPlayData = if (trickPlayManifest != null) { item.trickplayInfo?.get(sourceId)
jellyfinRepository.getTrickPlayData(
item.id,
trickPlayManifest.widthResolutions.max(),
)
} else { } else {
null null
} }
@ -78,12 +76,12 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex) downloadExternalMediaStreams(item, source, storageIndex)
if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo)
}
if (intro != null) { if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertIntro(intro.toIntroDto(item.id))
} }
if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
}
val request = DownloadManager.Request(source.path.toUri()) val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
@ -107,12 +105,12 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex) downloadExternalMediaStreams(item, source, storageIndex)
if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo)
}
if (intro != null) { if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertIntro(intro.toIntroDto(item.id))
} }
if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
}
val request = DownloadManager.Request(source.path.toUri()) val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
@ -175,8 +173,7 @@ class DownloaderImpl(
database.deleteIntro(item.id) database.deleteIntro(item.id)
database.deleteTrickPlayManifest(item.id) File(context.filesDir, "trickplay/${item.id}").deleteRecursively()
File(context.filesDir, "trickplay/${item.id}.bif").delete()
} }
override suspend fun getProgress(downloadId: Long?): Pair<Int, Int> { override suspend fun getProgress(downloadId: Long?): Pair<Int, Int> {
@ -233,14 +230,37 @@ class DownloaderImpl(
} }
} }
private fun downloadTrickPlay( private suspend fun downloadTrickplayData(
item: FindroidItem, itemId: UUID,
trickPlayManifest: TrickPlayManifest, sourceId: String,
byteArray: ByteArray, trickplayInfo: FindroidTrickplayInfo,
) { ) {
database.insertTrickPlayManifest(trickPlayManifest.toTrickPlayManifestDto(item.id)) val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt()
File(context.filesDir, "trickplay").mkdirs() val byteArrays = mutableListOf<ByteArray>()
val file = File(context.filesDir, "trickplay/${item.id}.bif") for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData(
itemId,
trickplayInfo.width,
i,
)?.let { byteArray ->
byteArrays.add(byteArray)
}
}
saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays)
}
private fun saveTrickplayData(
itemId: UUID,
sourceId: String,
trickplayInfo: FindroidTrickplayInfo,
byteArrays: List<ByteArray>,
) {
val basePath = "trickplay/$itemId/$sourceId"
database.insertTrickplayInfo(trickplayInfo.toFindroidTrickplayInfoDto(sourceId))
File(context.filesDir, basePath).mkdirs()
for ((i, byteArray) in byteArrays.withIndex()) {
val file = File(context.filesDir, "$basePath/$i")
file.writeBytes(byteArray) file.writeBytes(byteArray)
} }
}
} }

View file

@ -202,8 +202,10 @@ constructor(
appPreferences.currentServer = server.id appPreferences.currentServer = server.id
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = recommendedServerInfo.address api.update(
api.accessToken = null baseUrl = recommendedServerInfo.address,
accessToken = null,
)
} }
_uiState.emit(UiState.Normal) _uiState.emit(UiState.Normal)

View file

@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.models.FavoriteSection
import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -39,7 +40,10 @@ constructor(
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
val items = jellyfinRepository.getItems(parentId = parentId) val items = jellyfinRepository.getItems(
parentId = parentId,
sortBy = SortBy.RELEASE_DATE,
)
if (items.isEmpty()) { if (items.isEmpty()) {
_uiState.emit(UiState.Normal(emptyList())) _uiState.emit(UiState.Normal(emptyList()))

View file

@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.models.HomeSection
import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.toView import dev.jdtech.jellyfin.utils.toView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,13 +42,12 @@ class HomeViewModel @Inject internal constructor(
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.postCapabilities() repository.postCapabilities()
} catch (_: Exception) { } catch (_: Exception) { }
}
} }
} }
fun loadData() { fun loadData() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.Default) {
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
val items = mutableListOf<HomeItem>() val items = mutableListOf<HomeItem>()
@ -93,7 +93,7 @@ class HomeViewModel @Inject internal constructor(
private suspend fun loadViews() = repository private suspend fun loadViews() = repository
.getUserViews() .getUserViews()
.filter { view -> CollectionType.supported.any { it.type == view.collectionType } } .filter { view -> CollectionType.fromString(view.collectionType?.serialName) in CollectionType.supported }
.map { view -> view to repository.getLatestMedia(view.id) } .map { view -> view to repository.getLatestMedia(view.id) }
.filter { (_, latest) -> latest.isNotEmpty() } .filter { (_, latest) -> latest.isNotEmpty() }
.map { (view, latest) -> view.toView().apply { items = latest } } .map { (view, latest) -> view.toView().apply { items = latest } }

View file

@ -48,16 +48,20 @@ constructor(
CollectionType.Movies -> listOf(BaseItemKind.MOVIE) CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
CollectionType.TvShows -> listOf(BaseItemKind.SERIES) CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET) CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
CollectionType.Mixed -> listOf(BaseItemKind.FOLDER, BaseItemKind.MOVIE, BaseItemKind.SERIES)
else -> null else -> null
} }
val recursive = itemType == null || !itemType.contains(BaseItemKind.FOLDER)
viewModelScope.launch { viewModelScope.launch {
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
val items = jellyfinRepository.getItemsPaging( val items = jellyfinRepository.getItemsPaging(
parentId = parentId, parentId = parentId,
includeTypes = itemType, includeTypes = itemType,
recursive = true, recursive = recursive,
sortBy = sortBy, sortBy = if (libraryType == CollectionType.TvShows && sortBy == SortBy.DATE_PLAYED) SortBy.SERIES_DATE_PLAYED else sortBy, // Jellyfin uses a different enum for sorting series by data played
sortOrder = sortOrder, sortOrder = sortOrder,
).cachedIn(viewModelScope) ).cachedIn(viewModelScope)
_uiState.emit(UiState.Normal(items)) _uiState.emit(UiState.Normal(items))

View file

@ -32,7 +32,7 @@ constructor(
private val jellyfinApi: JellyfinApi, private val jellyfinApi: JellyfinApi,
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Normal) private val _uiState = MutableStateFlow<UiState>(UiState.Normal())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading) private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
val usersState = _usersState.asStateFlow() val usersState = _usersState.asStateFlow()
@ -44,8 +44,10 @@ constructor(
private var quickConnectJob: Job? = null private var quickConnectJob: Job? = null
private var loginDisclaimer: String? = null
sealed class UiState { sealed class UiState {
data object Normal : UiState() data class Normal(val disclaimer: String? = null) : UiState()
data object Loading : UiState() data object Loading : UiState()
data class Error(val message: UiText) : UiState() data class Error(val message: UiText) : UiState()
} }
@ -62,10 +64,18 @@ constructor(
} }
init { init {
loadDisclaimer()
loadPublicUsers() loadPublicUsers()
loadQuickConnectAvailable() loadQuickConnectAvailable()
} }
private fun loadDisclaimer() {
viewModelScope.launch {
loginDisclaimer = jellyfinApi.brandingApi.getBrandingOptions().content.loginDisclaimer
_uiState.emit(UiState.Normal(loginDisclaimer))
}
}
private fun loadPublicUsers() { private fun loadPublicUsers() {
viewModelScope.launch { viewModelScope.launch {
_usersState.emit(UsersState.Loading) _usersState.emit(UsersState.Loading)
@ -93,7 +103,7 @@ constructor(
private fun loadQuickConnectAvailable() { private fun loadQuickConnectAvailable() {
viewModelScope.launch { viewModelScope.launch {
try { try {
val isEnabled by jellyfinApi.quickConnectApi.getEnabled() val isEnabled by jellyfinApi.quickConnectApi.getQuickConnectEnabled()
if (isEnabled) { if (isEnabled) {
_quickConnectUiState.emit(QuickConnectUiState.Normal) _quickConnectUiState.emit(QuickConnectUiState.Normal)
} }
@ -121,7 +131,7 @@ constructor(
saveAuthenticationResult(authenticationResult) saveAuthenticationResult(authenticationResult)
_uiState.emit(UiState.Normal) _uiState.emit(UiState.Normal(loginDisclaimer))
eventsChannel.send(LoginEvent.NavigateToHome) eventsChannel.send(LoginEvent.NavigateToHome)
} catch (e: Exception) { } catch (e: Exception) {
val message = val message =
@ -144,12 +154,12 @@ constructor(
} }
quickConnectJob = viewModelScope.launch { quickConnectJob = viewModelScope.launch {
try { try {
var quickConnectState = jellyfinApi.quickConnectApi.initiate().content var quickConnectState = jellyfinApi.quickConnectApi.initiateQuickConnect().content
_quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code)) _quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
while (!quickConnectState.authenticated) { while (!quickConnectState.authenticated) {
quickConnectState = jellyfinApi.quickConnectApi.connect(quickConnectState.secret).content
delay(5000L) delay(5000L)
quickConnectState = jellyfinApi.quickConnectApi.getQuickConnectState(quickConnectState.secret).content
} }
val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect( val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
secret = quickConnectState.secret, secret = quickConnectState.secret,
@ -178,7 +188,7 @@ constructor(
insertUser(appPreferences.currentServer!!, user) insertUser(appPreferences.currentServer!!, user)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = authenticationResult.accessToken api.update(accessToken = authenticationResult.accessToken)
userId = authenticationResult.user?.id userId = authenticationResult.user?.id
} }
} }

View file

@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.FindroidCollection import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -35,9 +34,7 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
val items = jellyfinRepository.getLibraries() val collections = jellyfinRepository.getLibraries()
val collections =
items.filter { collection -> collection.type in CollectionType.supported }
_uiState.emit(UiState.Normal(collections)) _uiState.emit(UiState.Normal(collections))
} catch (e: Exception) { } catch (e: Exception) {
_uiState.emit( _uiState.emit(

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.MediaStream import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PersonKind
import org.jellyfin.sdk.model.api.VideoRangeType
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -119,7 +121,7 @@ constructor(
private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> { private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> {
val actors: List<BaseItemPerson> val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" } actors = item.people.filter { it.type == PersonKind.ACTOR }
} }
return actors return actors
} }
@ -127,7 +129,7 @@ constructor(
private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? { private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? {
val director: BaseItemPerson? val director: BaseItemPerson?
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" } director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
} }
return director return director
} }
@ -135,7 +137,7 @@ constructor(
private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> { private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> {
val writers: List<BaseItemPerson> val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" } writers = item.people.filter { it.type == PersonKind.WRITER }
} }
return writers return writers
} }
@ -213,9 +215,9 @@ constructor(
DisplayProfile.DOLBY_VISION DisplayProfile.DOLBY_VISION
} else { } else {
when (videoRangeType) { when (videoRangeType) {
DisplayProfile.HDR.raw -> DisplayProfile.HDR VideoRangeType.HDR10 -> DisplayProfile.HDR10
DisplayProfile.HDR10.raw -> DisplayProfile.HDR10 VideoRangeType.HDR10_PLUS -> DisplayProfile.HDR10_PLUS
DisplayProfile.HLG.raw -> DisplayProfile.HLG VideoRangeType.HLG -> DisplayProfile.HLG
else -> DisplayProfile.SDR else -> DisplayProfile.SDR
} }
}, },

View file

@ -1,5 +1,6 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -73,17 +74,29 @@ constructor(
server.currentServerAddressId = address.id server.currentServerAddressId = address.id
database.update(server) database.update(server)
jellyfinApi.api.baseUrl = address.address jellyfinApi.api.update(
baseUrl = address.address,
)
eventsChannel.send(ServerAddressesEvent.NavigateToHome) eventsChannel.send(ServerAddressesEvent.NavigateToHome)
} }
} }
fun addAddress(address: String) { fun addAddress(context: Context, address: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try {
val jellyfinApi = JellyfinApi(context)
jellyfinApi.api.update(
baseUrl = address,
)
val systemInfo by jellyfinApi.systemApi.getPublicSystemInfo()
if (systemInfo.id != currentServerId) {
return@launch
}
val serverAddress = ServerAddress(UUID.randomUUID(), currentServerId, address) val serverAddress = ServerAddress(UUID.randomUUID(), currentServerId, address)
database.insertServerAddress(serverAddress) database.insertServerAddress(serverAddress)
loadAddresses(currentServerId) loadAddresses(currentServerId)
} catch (_: Exception) { }
} }
} }
} }

View file

@ -101,8 +101,10 @@ constructor(
// If server has no selected user, navigate to login fragment // If server has no selected user, navigate to login fragment
if (user == null) { if (user == null) {
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = null baseUrl = serverAddress.address,
accessToken = null,
)
userId = null userId = null
} }
appPreferences.currentServer = server.id appPreferences.currentServer = server.id
@ -111,8 +113,10 @@ constructor(
} }
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user.accessToken baseUrl = serverAddress.address,
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -97,7 +97,7 @@ constructor(
nameStringResource = R.string.pref_player_mpv_vo, nameStringResource = R.string.pref_player_mpv_vo,
dependencies = listOf(Constants.PREF_PLAYER_MPV), dependencies = listOf(Constants.PREF_PLAYER_MPV),
backendName = Constants.PREF_PLAYER_MPV_VO, backendName = Constants.PREF_PLAYER_MPV_VO,
backendDefaultValue = "gpu", backendDefaultValue = "gpu-next",
options = R.array.mpv_vos, options = R.array.mpv_vos,
optionValues = R.array.mpv_vos, optionValues = R.array.mpv_vos,
), ),

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.PersonKind
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -100,7 +101,7 @@ constructor(
private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> { private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> {
val actors: List<BaseItemPerson> val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" } actors = item.people.filter { it.type == PersonKind.ACTOR }
} }
return actors return actors
} }
@ -108,7 +109,7 @@ constructor(
private suspend fun getDirector(item: FindroidShow): BaseItemPerson? { private suspend fun getDirector(item: FindroidShow): BaseItemPerson? {
val director: BaseItemPerson? val director: BaseItemPerson?
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" } director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
} }
return director return director
} }
@ -116,7 +117,7 @@ constructor(
private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> { private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> {
val writers: List<BaseItemPerson> val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" } writers = item.people.filter { it.type == PersonKind.WRITER }
} }
return writers return writers
} }

View file

@ -19,7 +19,7 @@ import javax.inject.Inject
class UserSelectViewModel class UserSelectViewModel
@Inject @Inject
constructor( constructor(
private val appPreferences: AppPreferences, appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi, private val jellyfinApi: JellyfinApi,
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
) : ViewModel() { ) : ViewModel() {
@ -71,7 +71,9 @@ constructor(
database.update(server) database.update(server)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = user.accessToken api.update(
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -73,7 +73,9 @@ constructor(
database.update(server) database.update(server)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = user.accessToken api.update(
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -40,8 +40,10 @@ class SyncWorker @AssistedInject constructor(
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue
for (user in serverWithAddressesAndUsers.users) { for (user in serverWithAddressesAndUsers.users) {
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user.accessToken baseUrl = serverAddress.address,
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }
val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) } val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) }
@ -66,17 +68,16 @@ class SyncWorker @AssistedInject constructor(
try { try {
when (userData.played) { when (userData.played) {
true -> jellyfinApi.playStateApi.markPlayedItem(user.id, item.id) true -> jellyfinApi.playStateApi.markPlayedItem(item.id, user.id)
false -> jellyfinApi.playStateApi.markUnplayedItem(user.id, item.id) false -> jellyfinApi.playStateApi.markUnplayedItem(item.id, user.id)
} }
when (userData.favorite) { when (userData.favorite) {
true -> jellyfinApi.userLibraryApi.markFavoriteItem(user.id, item.id) true -> jellyfinApi.userLibraryApi.markFavoriteItem(item.id, user.id)
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(user.id, item.id) false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(item.id, user.id)
} }
jellyfinApi.playStateApi.onPlaybackStopped( jellyfinApi.playStateApi.onPlaybackStopped(
userId = user.id,
itemId = item.id, itemId = item.id,
positionTicks = userData.playbackPositionTicks, positionTicks = userData.playbackPositionTicks,
) )

View file

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="24dp"
android:viewportWidth="36"
android:viewportHeight="24">
<path
android:pathData="M14,19l9,-7l-9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M2,19l9,-7l-9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M26,19l9,-7l-9,-7l0,14z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -138,7 +138,6 @@
<string name="pref_player_intro_skipper_summary">Requiere que el complemento Intro Skipper de ConfusedPolarBear esté instalado en el servidor</string> <string name="pref_player_intro_skipper_summary">Requiere que el complemento Intro Skipper de ConfusedPolarBear esté instalado en el servidor</string>
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string> <string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
<string name="episode_name_extended_with_end">T%1$d:E%2$d-%3$d - %4$s</string> <string name="episode_name_extended_with_end">T%1$d:E%2$d-%3$d - %4$s</string>
<string name="pref_player_trick_play_summary">Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor</string>
<string name="extra_info_summary">Mostrar información detallada de audio, video y subtítulos</string> <string name="extra_info_summary">Mostrar información detallada de audio, video y subtítulos</string>
<string name="offline_mode">Modo desconectado</string> <string name="offline_mode">Modo desconectado</string>
<string name="offline_mode_icon">Icono de modo desconectado</string> <string name="offline_mode_icon">Icono de modo desconectado</string>
@ -161,7 +160,6 @@
<string name="player_gestures_seek">Gesto de búsqueda</string> <string name="player_gestures_seek">Gesto de búsqueda</string>
<string name="player_gestures_seek_summary">Deslizar horizontalmente para buscar adelante o atrás</string> <string name="player_gestures_seek_summary">Deslizar horizontalmente para buscar adelante o atrás</string>
<string name="downloaded_indicator">Indicador de descargado</string> <string name="downloaded_indicator">Indicador de descargado</string>
<string name="pref_player_trick_play">Miniaturas (Trick Play)</string>
<string name="storage_name">%1$s (%2$d MB libres)</string> <string name="storage_name">%1$s (%2$d MB libres)</string>
<string name="preparing_download">Preparando descarga</string> <string name="preparing_download">Preparando descarga</string>
<string name="cancel_download">Cancelar descarga</string> <string name="cancel_download">Cancelar descarga</string>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="add_server_error_version">Неподдържана версия на сървъра: %1$s. Моля ъпдейтнете вашия сървър</string> <string name="add_server_error_version">Неподдържана версия на сървъра: %1$s. Моля ъпдейтнете вашия сървър</string>
<string name="add_server_error_no_id">Сървърът няма id, изглежда, че има нещо не е наред със сървъра</string> <string name="add_server_error_no_id">Сървърът няма id, изглежда, че нещо не е наред със сървъра</string>
<string name="remove_server">Премахнете сървъра</string> <string name="remove_server">Премахване на сървъра</string>
<string name="sort_by">Сортирайте по</string> <string name="sort_by">Сортирайте по</string>
<string name="download_roaming">Свалете при роуминг</string> <string name="download_roaming">Свалете при роуминг</string>
<string name="mpv_player_summary">Използвайте експерименталният mpv плейър за да пускане на видеа. mpv поддържа повече кодекси за видео, аудио и субтитри.</string> <string name="mpv_player_summary">Използвайте експерименталният mpv плейър за да пускане на видеа. mpv поддържа повече кодекси за видео, аудио и субтитри.</string>
@ -13,11 +13,11 @@
<string name="login_error_wrong_username_password">Грешно потребителско име или парола</string> <string name="login_error_wrong_username_password">Грешно потребителско име или парола</string>
<string name="add_server_error_not_jellyfin">Не е Jellyfin сървър: %1$s</string> <string name="add_server_error_not_jellyfin">Не е Jellyfin сървър: %1$s</string>
<string name="add_server_error_slow">Сървърът реагира твърде бавно: %1$s</string> <string name="add_server_error_slow">Сървърът реагира твърде бавно: %1$s</string>
<string name="remove">Премахнете</string> <string name="remove">Премахване</string>
<string name="cancel">Отменете</string> <string name="cancel">Отменяне</string>
<string name="title_home">Начало</string> <string name="title_home">Начало</string>
<string name="button_connect">Свържете се</string> <string name="button_connect">Свързване</string>
<string name="button_login">Влезнете</string> <string name="button_login">Вход</string>
<string name="remove_server_dialog_text">Сигурни ли сте, че искате да премахнете сървъра %1$s</string> <string name="remove_server_dialog_text">Сигурни ли сте, че искате да премахнете сървъра %1$s</string>
<string name="title_media">Моята медия</string> <string name="title_media">Моята медия</string>
<string name="edit_text_server_address_hint">Адрес на сървъра</string> <string name="edit_text_server_address_hint">Адрес на сървъра</string>
@ -27,10 +27,10 @@
<string name="title_settings">Настройки</string> <string name="title_settings">Настройки</string>
<string name="title_download">Свалени</string> <string name="title_download">Свалени</string>
<string name="view_all">Вижте всички</string> <string name="view_all">Вижте всички</string>
<string name="error_loading_data">Проблем с зареждането на датата</string> <string name="error_loading_data">Проблем с зареждането на информацията</string>
<string name="retry">Опитайте отново</string> <string name="retry">Опитайте отново</string>
<string name="genres">Жанрове</string> <string name="genres">Жанрове</string>
<string name="director">Директор</string> <string name="director">Режисьор</string>
<string name="writers">Писатели</string> <string name="writers">Писатели</string>
<string name="seasons">Сезони</string> <string name="seasons">Сезони</string>
<string name="play_button_description">Пуснете медията</string> <string name="play_button_description">Пуснете медията</string>
@ -46,7 +46,7 @@
<string name="no_downloads">Вие нямате нищо свалено</string> <string name="no_downloads">Вие нямате нищо свалено</string>
<string name="search">Търсене</string> <string name="search">Търсене</string>
<string name="settings_category_language">Език</string> <string name="settings_category_language">Език</string>
<string name="settings_preferred_audio_language">Предпочитан аудо език</string> <string name="settings_preferred_audio_language">Предпочитан аудио език</string>
<string name="settings_preferred_subtitle_language">Предпочитан език на субтитрите</string> <string name="settings_preferred_subtitle_language">Предпочитан език на субтитрите</string>
<string name="settings_category_servers">Сървъри</string> <string name="settings_category_servers">Сървъри</string>
<string name="settings_category_player">Плейър</string> <string name="settings_category_player">Плейър</string>
@ -57,7 +57,7 @@
<string name="shows_label">ТВ Предавания</string> <string name="shows_label">ТВ Предавания</string>
<string name="hide">Скрийте</string> <string name="hide">Скрийте</string>
<string name="sort_order">Ред на сортиране</string> <string name="sort_order">Ред на сортиране</string>
<string name="download_mobile_data">Свалете с мобилна дата</string> <string name="download_mobile_data">Свалете с мобилни данни</string>
<string name="ascending">Възходящ</string> <string name="ascending">Възходящ</string>
<string name="mpv_player">mpv плейър</string> <string name="mpv_player">mpv плейър</string>
<string name="download_button_description">Свалете</string> <string name="download_button_description">Свалете</string>
@ -71,7 +71,7 @@
<string name="error_preparing_player_items">Проблем при подготовката на елементите на плейъра.</string> <string name="error_preparing_player_items">Проблем при подготовката на елементите на плейъра.</string>
<string name="view_details">Вижте детайли</string> <string name="view_details">Вижте детайли</string>
<string name="view_details_underlined"><u>Вижте детайли</u></string> <string name="view_details_underlined"><u>Вижте детайли</u></string>
<string name="about">За</string> <string name="about">Описание</string>
<string name="privacy_policy">Политика за поверителност</string> <string name="privacy_policy">Политика за поверителност</string>
<string name="app_info">Информация за приложението</string> <string name="app_info">Информация за приложението</string>
<string name="unknown_error">Непозната грешка</string> <string name="unknown_error">Непозната грешка</string>
@ -84,95 +84,110 @@
<string name="series_poster">Постер на сериала</string> <string name="series_poster">Постер на сериала</string>
<string name="no_search_results">Няма резултати от търсенето</string> <string name="no_search_results">Няма резултати от търсенето</string>
<string name="settings_category_download">Свалени</string> <string name="settings_category_download">Свалени</string>
<string name="settings_use_cache_summary">Кеширайте снимките на диска за да ускорите времето за зареждане. Ще има ефект след рестартиране на приложението.</string> <string name="settings_use_cache_summary">Кеширайте снимките на диска, за да ускорите времето за зареждане. Ще има ефект след рестартиране на приложението.</string>
<string name="settings_cache_size_message">Приложението ще използва това количество MB от вашето дисково пространство, за да съхранява изображения от сървъра на Jellyfin. По-големи стойности може да са от полза при по-бавни мрежи.</string> <string name="settings_cache_size_message">Приложението ще използва това количество MB от вашето дисково пространство, за да съхранява изображения от сървъра на Jellyfin. По-големи стойности може да са от полза при по-бавни мрежи.</string>
<string name="descending">Низходящ</string> <string name="descending">Низходящ</string>
<string name="track_selection">[%1$s] %2$s (%3$s)</string> <string name="track_selection">[%1$s] %2$s (%3$s)</string>
<string name="runtime_minutes">%1$d мин</string> <string name="runtime_minutes">%1$d мин</string>
<string name="size">Tamanho</string> <string name="size">Размер</string>
<string name="seeking">Buscando</string> <string name="seeking">Търсене</string>
<string name="settings_request_timeout">Tempo limite da solicitação (ms)</string> <string name="settings_request_timeout">Лимит на заявките (ms)</string>
<string name="pref_player_mpv_ao">Saída de áudio</string> <string name="pref_player_mpv_ao">Изход на аудиото</string>
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string> <string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string> <string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
<string name="image_description_backdrop">Pano de fundo de %1$s</string> <string name="image_description_backdrop">Фон на %1$s</string>
<string name="users">Usuários</string> <string name="users">Потребители</string>
<string name="no_server_connection">Sem conexão com o servidor Jellyfin, para assistir off-line, ative o modo off-line</string> <string name="no_server_connection">Няма връзка към Jellyfin сървъра, за да гледате офлайн пуснете Режим без Интернет</string>
<string name="privacy_policy_notice">Ao usar o Findroid, você concorda com a <a href="https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY">Política de Privacidade</a>, que afirma que não coletamos quaisquer dados</string> <string name="privacy_policy_notice">При ползването на Findroid, вие се съгласявате с <a href="https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY">Политиката за поверителност</a> , която гласи, че не събираме никаква информация</string>
<string name="app_description">Aplicativo Jellyfin nativo de terceiros</string> <string name="app_description">Неофициално приложение за Jellyfin с \"native\" елементи</string>
<string name="dynamic_colors">Cores dinâmicas</string> <string name="dynamic_colors">Динамични цветове</string>
<string name="person_detail_title">Detalhes</string> <string name="person_detail_title">Детайли</string>
<string name="player_gestures_zoom_summary">Aperte para preencher a tela com o vídeo</string> <string name="player_gestures_zoom_summary">Плъзнете с два пръста, за да изпълните екрана</string>
<string name="player_brightness_remember">Lembre-se do nível de brilho</string> <string name="player_brightness_remember">Запомни ниво на яркост</string>
<string name="player_gestures_vb_summary">Deslize para cima e para baixo no lado direito da tela para alterar o volume e no lado esquerdo para alterar o brilho</string> <string name="player_gestures_vb_summary">Плъзнете нагоре и надолу на дяснатата страна на екрана, за да промените звука, и на лявата страна, за да промените яркостта</string>
<string name="sort_by_options_1">Classificação IMDB</string> <string name="sort_by_options_1">IMDB оценка</string>
<string name="sort_by_options_3">data adicionada</string> <string name="sort_by_options_3">Дата Добавен</string>
<string name="seek_back_increment">Buscar incremento de volta (ms)</string> <string name="seek_back_increment">Инкремент за връщане назад(ms)</string>
<string name="pref_player_mpv_vo">Saida de video</string> <string name="pref_player_mpv_vo">Изхода на видеото</string>
<string name="pref_player_intro_skipper_summary">Requer que o plugin Confused Polar Bears Intro Skipper esteja instalado no servidor</string> <string name="pref_player_intro_skipper_summary">Изисква Confused Polar Bears Intro Skipper да бъде инсталиран на сървъра</string>
<string name="remove_user">Remover usuário</string> <string name="remove_user">Премахване на потребител</string>
<string name="remove_user_dialog_text">Tem certeza de que deseja remover o usuário %1$s</string> <string name="remove_user_dialog_text">Сигурни ли сте, че искате да премахнете потребител %1$s</string>
<string name="quick_connect">Conexão rápida</string> <string name="quick_connect">Бързо Свързване</string>
<string name="extra_info_summary">Exibe informações detalhadas sobre áudio, vídeo e legendas</string> <string name="extra_info_summary">Показва детайлна информация за Аудио, Видео и Субтитри</string>
<string name="subtitle_chip_text">CC</string> <string name="subtitle_chip_text">Затворени субтитри</string>
<string name="temp">Temperatura</string> <string name="temp">временно</string>
<string name="offline_mode_icon">Ícone do modo off-line</string> <string name="offline_mode_icon">Режим без Интернет иконка</string>
<string name="offline_mode_go_online">Fique online</string> <string name="offline_mode_go_online">Свържи се с Интернет</string>
<string name="select_storage_location">Selecione o local de armazenamento</string> <string name="select_storage_location">Изберете локация на диска</string>
<string name="theme_light">Luz</string> <string name="theme_light">Светла</string>
<string name="amoled_theme_summary">Use tema AMOLED com fundo preto puro</string> <string name="amoled_theme_summary">Използвайте AMOLED тема с чисто черен фон</string>
<string name="theme_system">Siga o sistema</string> <string name="theme_system">Следвай системата</string>
<string name="libraries">Bibliotecas</string> <string name="libraries">Библиотеки</string>
<string name="amoled_theme">Tema escuro AMOLED</string> <string name="amoled_theme">AMOLED тъмна тема</string>
<string name="player_gestures_seek">Procure gesto</string> <string name="player_gestures_seek">Жест за търсене</string>
<string name="player_gestures_seek_summary">Deslize horizontalmente para avançar ou retroceder</string> <string name="player_gestures_seek_summary">Плъзнете хоризонтално, за да върнете назад или да пропуснете напред</string>
<string name="downloaded_indicator">Indicador baixado</string> <string name="downloaded_indicator">Индикатор за сваленост</string>
<string name="app_language">Idioma do aplicativo</string> <string name="app_language">Език на приложението</string>
<string name="episodes_label">Episódios</string> <string name="episodes_label">Епизоди</string>
<string name="image_description_poster">pôster de %1$s</string> <string name="image_description_poster">Постер на %1$s</string>
<string name="player_gestures">Gestos do jogador</string> <string name="player_gestures">Жестове на плейъра</string>
<string name="player_gestures_vb">Gestos de volume e brilho</string> <string name="player_gestures_vb">Жестове за яркост и звук</string>
<string name="sort_by_options_0">Título</string> <string name="sort_by_options_0">Заглавие</string>
<string name="dynamic_colors_summary">Use cores dinâmicas do Material You (disponível apenas no Android 12+)</string> <string name="dynamic_colors_summary">Използвай динамични Material You цветове (възможно единствено на Android 12+)</string>
<string name="sort_by_options_2">Avaliação parental</string> <string name="sort_by_options_2">Оценка за родителски контрол</string>
<string name="sort_by_options_4">Data de reprodução</string> <string name="sort_by_options_4">Дата пускан</string>
<string name="seek_forward_increment">Procure incremento direto (ms)</string> <string name="seek_forward_increment">Инкремент за пропускане напред(ms)</string>
<string name="subtitles">Legendas</string> <string name="subtitles">Субтитри</string>
<string name="subtitles_summary">Personalize a aparência das legendas</string> <string name="subtitles_summary">Персонализирайте външния вид на субтитрите</string>
<string name="settings_category_network">Rede</string> <string name="settings_category_network">Мрежа</string>
<string name="settings_socket_timeout">Tempo limite do soquete (ms)</string> <string name="settings_socket_timeout">Лимит на сокета (ms)</string>
<string name="pref_player_mpv_hwdec">Decodificação de hardware</string> <string name="pref_player_mpv_hwdec">Хардуерно декодиране</string>
<string name="pref_player_trick_play_summary">Requer que o plugin Jellyscrub do Nicknsy esteja instalado no servidor</string> <string name="add_address">Добави адрес</string>
<string name="add_address">Adicionar endereço</string> <string name="audio">Аудио</string>
<string name="audio">Audio</string> <string name="video">Видео</string>
<string name="video">Vídeo</string> <string name="extra_info">Покажи Допълнителна информация</string>
<string name="extra_info">Exibir informações extras</string> <string name="external">Външно</string>
<string name="external">Externo</string> <string name="storage_name">%1$s (%2$d MB свободни)</string>
<string name="storage_name">%1$s (%2$d MB grátis)</string> <string name="cancel_download">Отмени свалянето</string>
<string name="cancel_download">Cancelar transferência</string> <string name="preparing_download">Подготвяне на свалянето</string>
<string name="preparing_download">Preparando transferência</string> <string name="cancel_download_message">Сигурни ли сте, че искате на отмените свалянето?</string>
<string name="cancel_download_message">Tem certeza de que deseja cancelar a transferência\?</string> <string name="stop_download">Спри свалянето</string>
<string name="stop_download">Pare de baixar</string> <string name="storage_unavailable">Локацията на диска е недостъпна</string>
<string name="storage_unavailable">O local de armazenamento não está disponível</string> <string name="internal">Вътрежно</string>
<string name="internal">Interno</string> <string name="remove_server_address">Премахване на адрес на сървър</string>
<string name="remove_server_address">Remover endereço do servidor</string> <string name="remove_server_address_dialog_text">Сигурни ли сте, че искате да премахнете адреса на сървъра %1$s</string>
<string name="remove_server_address_dialog_text">Tem certeza de que deseja remover o endereço do servidor %1$s</string> <string name="sort_by_options_5">Дата на издаване</string>
<string name="sort_by_options_5">Data de lançamento</string> <string name="select_video_version_title">Изберете версия</string>
<string name="select_video_version_title">Selecione a versão</string> <string name="theme_dark">Тъмна</string>
<string name="theme_dark">Escuro</string> <string name="pref_player_intro_skipper">Пропускане на интрота</string>
<string name="pref_player_intro_skipper">Capitão de introdução</string> <string name="addresses">Адреси</string>
<string name="pref_player_trick_play">Truque</string> <string name="add_server_address">Добави адрес на сървър</string>
<string name="addresses">Endereços</string> <string name="player_gestures_zoom">Жест за приближаване (zoom)</string>
<string name="add_server_address">Adicionar endereço do servidor</string> <string name="settings_connect_timeout">Лимит на свързването (ms)</string>
<string name="player_gestures_zoom">Gesto de zoom</string> <string name="add_user">Добави потребител</string>
<string name="settings_connect_timeout">Tempo limite de conexão (ms)</string> <string name="add">Добави</string>
<string name="add_user">Adicionar usuário</string> <string name="picture_in_picture">Режим Картина-в-картина</string>
<string name="add">Adicionar</string> <string name="picture_in_picture_gesture">Жест за Режим Картина-в-картина</string>
<string name="picture_in_picture">Imagem em imagem</string> <string name="picture_in_picture_gesture_summary">Използайте Home бутона или жест, за да влезете в режим Картина-в-картина</string>
<string name="picture_in_picture_gesture">Gesto inicial picture-in-picture</string> <string name="subtitle">Субтитри</string>
<string name="picture_in_picture_gesture_summary">Use o botão home ou gesto para entrar picture-in-picture enquanto o vídeo está sendo reproduzido</string> <string name="offline_mode">Режим без Интернет</string>
<string name="subtitle">Legendas</string> <string name="downloading_error">Грешка при сваляне</string>
<string name="offline_mode">Modo offline</string> <string name="not_enough_storage">Тази медия изисква %1$s свободно място, но има само %2$s свободно</string>
<string name="downloading_error">Erro ao baixar</string> <string name="remove_from_favorites">Махни от любими</string>
<string name="not_enough_storage">Este item requer %1$s de armazenamento gratuito, mas apenas %2$s está disponível</string> <string name="collection_no_media">Тази колекция не съдържа никаква медия</string>
<string name="player_start_maximized">Започни в максимизиран режим</string>
<string name="player_start_maximized_summary">Отвори видео в максимизиран режим по подразбиране</string>
<string name="player_gestures_chapter_skip_summary">Дълго задържане на Лявата / Дясната страна, за пропускане на глава (отменя жеста за 2x скорост)</string>
<string name="pref_player_chapter_markers">Маркери за глави</string>
<string name="pref_player_chapter_markers_summary">Показвай маркери за глави на времевата линия</string>
<string name="no_servers_found">Не са намерени сървъри</string>
<string name="no_users_found">Не са намерени потребители</string>
<string name="select_user">Избери потребител</string>
<string name="live_tv">Телевизия На Живо</string>
<string name="play">Пусни</string>
<string name="mark_as_played">Маркирай като гледано</string>
<string name="unmark_as_played">Махни маркирано като гледано</string>
<string name="watch_trailer">Гледай трейлър</string>
<string name="add_to_favorites">Добави към любими</string>
<string name="player_gestures_chapter_skip">Пропусни глава</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show more