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
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle
run: ./gradlew lintDebug ktlintCheck
assemble:
@ -29,14 +29,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: temurin
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle
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.

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
*.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
![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.
@ -8,7 +14,7 @@ I am developing this application in my spare time.
**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
| Home | Library | Movie | Season | Episode |
@ -23,7 +29,7 @@ I am developing this application in my spare time.
- ExoPlayer
- Video codecs: H.263, H.264, H.265, VP8, VP9, AV1
- 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
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
- 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
- Optionally force software decoding when hardware decoding has issues.
- Picture-in-picture mode
- Media chapters
- Timeline markers
- Chapter navigation gestures
## Planned features
- Android TV

View file

@ -21,6 +21,20 @@ android {
versionCode = Versions.appCode
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 {
@ -47,9 +61,6 @@ android {
dimension = "variant"
isDefault = true
}
register("huawei") {
dimension = "variant"
}
}
splits {
@ -61,6 +72,8 @@ android {
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = Versions.java
targetCompatibility = Versions.java
}
@ -78,11 +91,11 @@ ktlint {
}
dependencies {
implementation(project(":core"))
implementation(project(":data"))
implementation(project(":preferences"))
implementation(project(":player:core"))
implementation(project(":player:video"))
implementation(projects.core)
implementation(projects.data)
implementation(projects.preferences)
implementation(projects.player.core)
implementation(projects.player.video)
implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries)
implementation(libs.androidx.activity)
@ -90,9 +103,7 @@ dependencies {
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.navigation.fragment)
@ -109,7 +120,14 @@ dependencies {
implementation(libs.jellyfin.core)
compileOnly(libs.libmpv)
implementation(libs.material)
implementation(libs.media3.ffmpeg.decoder)
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.request.CachePolicy
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
import dev.jdtech.jellyfin.core.R as CoreR
@HiltAndroidApp
class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactory {
@ -40,7 +42,12 @@ class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactor
"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 {

View file

@ -8,7 +8,6 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import androidx.media3.exoplayer.trackselection.MappingTrackSelector
import androidx.media3.session.MediaSession
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
@ -72,19 +71,6 @@ abstract class BasePlayerActivity : AppCompatActivity() {
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) {
playerControls.setOnApplyWindowInsetsListener { _, windowInsets ->
val cutout = windowInsets.displayCutout

View file

@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity() {
private fun applyTheme() {
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.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.provider.Settings
import android.util.Rational
import android.view.View
import android.view.WindowManager
@ -27,15 +29,14 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.C
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import androidx.navigation.navArgs
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
import dev.jdtech.jellyfin.utils.PreviewScrubListener
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
@ -56,6 +57,7 @@ class PlayerActivity : BasePlayerActivity() {
private var playerGestureHelper: PlayerGestureHelper? = null
override val viewModel: PlayerActivityViewModel by viewModels()
private var previewScrubListener: PreviewScrubListener? = null
private var wasZoom: Boolean = false
private val isPipSupported by lazy {
// Check if device has PiP feature
@ -112,10 +114,6 @@ class PlayerActivity : BasePlayerActivity() {
finish()
}
binding.playerView.findViewById<View>(R.id.back_button_alt).setOnClickListener {
finish()
}
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
@ -143,9 +141,21 @@ class PlayerActivity : BasePlayerActivity() {
}
}
// Trick Play
// Trickplay
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
@ -169,6 +179,13 @@ class PlayerActivity : BasePlayerActivity() {
viewModel.eventsChannelFlow.collect { event ->
when (event) {
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()
}
if (appPreferences.playerTrickPlay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
// Set marker color
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(
imagePreview,
timeBar,
@ -254,7 +274,7 @@ class PlayerActivity : BasePlayerActivity() {
hideSystemUI()
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
@ -263,12 +283,17 @@ class PlayerActivity : BasePlayerActivity() {
}
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()
}
}
private fun pipParams(): PictureInPictureParams {
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
val aspectRatio = binding.playerView.player?.videoSize?.let {
@ -296,24 +321,21 @@ class PlayerActivity : BasePlayerActivity() {
)
}
return PictureInPictureParams.Builder()
val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.setSourceRectHint(sourceRectHint)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(enableAutoEnter)
}
return builder.build()
}
private fun pictureInPicture() {
if (!isPipSupported) {
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 {
enterPictureInPictureMode(pipParams())
@ -325,8 +347,35 @@ class PlayerActivity : BasePlayerActivity() {
newConfig: Configuration,
) {
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
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
import android.text.Html.fromHtml
import android.util.TypedValue
import android.view.LayoutInflater
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.episodeOverview.text = episode.overview
binding.episodeOverview.text = fromHtml(episode.overview, 0)
if (episode.playbackPositionTicks > 0) {
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 kotlinx.coroutines.launch
import timber.log.Timber
import dev.jdtech.jellyfin.core.R as CoreR
@AndroidEntryPoint
class CollectionFragment : Fragment() {
@ -40,6 +41,8 @@ class CollectionFragment : Fragment() {
): View {
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
binding.noFavoritesText.text = getString(CoreR.string.collection_no_media)
binding.favoritesRecyclerView.adapter = FavoritesListAdapter { item ->
navigateToMediaItem(item)
}

View file

@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.fragments
import android.app.DownloadManager
import android.os.Bundle
import android.text.Html.fromHtml
import android.text.format.Formatter
import android.util.TypedValue
import android.view.LayoutInflater
@ -285,11 +286,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}
binding.seriesName.text = episode.seriesName
binding.overview.text = episode.overview
binding.overview.text = fromHtml(episode.overview, 0)
binding.year.text = formatDateTime(episode.premiereDate)
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.isVisible = true
}
binding.missingIcon.isVisible = false
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.SortDialogFragment
import dev.jdtech.jellyfin.models.FindroidBoxSet
import dev.jdtech.jellyfin.models.FindroidFolder
import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie
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
import android.os.Bundle
import android.text.Html.fromHtml
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -82,7 +83,7 @@ class LoginFragment : Fragment() {
viewModel.uiState.collect { uiState ->
Timber.d("$uiState")
when (uiState) {
is LoginViewModel.UiState.Normal -> bindUiStateNormal()
is LoginViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
}
@ -135,11 +136,15 @@ class LoginFragment : Fragment() {
return binding.root
}
private fun bindUiStateNormal() {
private fun bindUiStateNormal(uiState: LoginViewModel.UiState.Normal) {
binding.buttonLogin.isEnabled = true
binding.progressCircular.isVisible = false
binding.editTextUsernameLayout.isEnabled = true
binding.editTextPasswordLayout.isEnabled = true
uiState.disclaimer?.let { disclaimer ->
binding.loginDisclaimer.text = fromHtml(disclaimer, 0)
}
}
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {

View file

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

View file

@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html.fromHtml
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -170,7 +171,6 @@ class ShowFragment : Fragment() {
if (item.trailer != null) {
binding.itemActions.trailerButton.isVisible = true
}
binding.communityRating.isVisible = item.communityRating != null
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
@ -212,9 +212,12 @@ class ShowFragment : Fragment() {
binding.playtime.text = runTime
}
binding.officialRating.text = item.officialRating
item.communityRating?.also {
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.genresGroup.isVisible = item.genres.isNotEmpty()
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.PlayerActivity
import dev.jdtech.jellyfin.isControlsLocked
import dev.jdtech.jellyfin.models.PlayerChapter
import dev.jdtech.jellyfin.mpv.MPVPlayer
import timber.log.Timber
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.
* 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).
@ -55,9 +56,14 @@ class PlayerGestureHelper(
private var lastScaleEvent: Long = 0
private var playbackSpeedIncrease: Float = 2f
private var lastPlaybackSpeed: Float = 0f
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
private var currentNumberOfPointers: Int = 0
private val tapGestureDetector = GestureDetector(
playerView.context,
object : GestureDetector.SimpleOnGestureListener() {
@ -69,6 +75,22 @@ class PlayerGestureHelper(
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 {
// Disables double tap gestures if view is locked
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() {
val currentPosition = playerView.player?.currentPosition ?: 0
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
@ -315,8 +386,8 @@ class PlayerGestureHelper(
lastScaleEvent = SystemClock.elapsedRealtime()
val scaleFactor = detector.scaleFactor
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
isZoomEnabled = scaleFactor > 1
updateZoomMode(isZoomEnabled)
val enableZoom = scaleFactor > 1
updateZoomMode(enableZoom)
}
return true
}
@ -325,16 +396,17 @@ class PlayerGestureHelper(
},
).apply { isQuickScaleEnabled = false }
private fun updateZoomMode(enabled: Boolean) {
fun updateZoomMode(enabled: Boolean) {
if (playerView.player is MPVPlayer) {
(playerView.player as MPVPlayer).updateZoomMode(enabled)
} else {
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
}
isZoomEnabled = enabled
}
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 {
if (visibility == View.VISIBLE) {
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
@ -361,6 +433,12 @@ class PlayerGestureHelper(
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
}
updateZoomMode(appPreferences.playerStartMaximized)
@Suppress("ClickableViewAccessibility")
playerView.setOnTouchListener { _, event ->
if (playerView.useController) {
currentNumberOfPointers = event.pointerCount
when (event.pointerCount) {
1 -> {
tapGestureDetector.onTouchEvent(event)

View file

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

View file

@ -113,6 +113,37 @@
tools:ignore="ContentDescription" />
</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
android:id="@+id/image_ffwd_animation_ripple"
android:layout_width="50dp"

View file

@ -8,43 +8,6 @@
android:visibility="gone"
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
android:id="@+id/btn_unlock"
android:layout_width="wrap_content"
@ -53,10 +16,8 @@
android:background="@drawable/rounded_corner"
android:contentDescription="@string/select_playback_speed"
android:padding="16dp"
android:layout_margin="8dp"
android:src="@drawable/ic_unlock"
app:tint="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

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

View file

@ -141,6 +141,15 @@
android:visibility="invisible" />
</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>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

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

View file

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

View file

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

View file

@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -71,7 +70,6 @@ fun AddServerScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun AddServerScreenLayout(
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.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination
@ -88,7 +87,6 @@ fun HomeScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun HomeScreenLayout(
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.TvLazyVerticalGrid
import androidx.tv.foundation.lazy.grid.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@ -50,7 +49,6 @@ fun LibrariesScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun LibrariesScreenLayout(
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.TvGridItemSpan
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import dev.jdtech.jellyfin.destinations.LibraryScreenDestination
import dev.jdtech.jellyfin.destinations.MovieScreenDestination
import dev.jdtech.jellyfin.destinations.ShowScreenDestination
import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.FindroidFolder
import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidShow
@ -65,12 +66,14 @@ fun LibraryScreen(
is FindroidShow -> {
navigator.navigate(ShowScreenDestination(item.id))
}
is FindroidFolder -> {
navigator.navigate(LibraryScreenDestination(item.id, item.name, libraryType))
}
}
},
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun LibraryScreenLayout(
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.hilt.navigation.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -86,7 +86,6 @@ fun LoginScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun LoginScreenLayout(
uiState: LoginViewModel.UiState,
@ -110,6 +109,14 @@ private fun LoginScreenLayout(
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 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() {
FindroidTheme {
LoginScreenLayout(
uiState = LoginViewModel.UiState.Normal,
uiState = LoginViewModel.UiState.Normal(),
quickConnectUiState = LoginViewModel.QuickConnectUiState.Normal,
onLoginClick = { _, _ -> },
onQuickConnectClick = {},

View file

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

View file

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

View file

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

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination
@ -76,7 +75,6 @@ fun SeasonScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun SeasonScreenLayout(
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.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.OutlinedButton
@ -103,7 +102,6 @@ fun ServerSelectScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun ServerSelectScreenLayout(
uiState: ServerSelectViewModel.UiState,
@ -246,7 +244,6 @@ private fun ServerSelectScreenLayoutPreviewNoServers() {
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun ServerComponent(
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.TvLazyVerticalGrid
import androidx.tv.foundation.lazy.grid.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination
@ -80,7 +79,6 @@ fun SettingsScreen(
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun SettingsScreenLayout(
uiState: SettingsViewModel.UiState,

View file

@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.ramcosta.composedestinations.annotation.Destination
@ -90,7 +89,6 @@ fun SettingsSubScreen(
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun SettingsSubScreenLayout(
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.rememberTvLazyListState
import androidx.tv.material3.Button
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
@ -126,7 +125,6 @@ fun ShowScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun ShowScreenLayout(
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.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.OutlinedButton
@ -99,7 +98,6 @@ fun UserSelectScreen(
)
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun UserSelectScreenLayout(
uiState: UserSelectViewModel.UiState,
@ -204,7 +202,6 @@ private fun UserSelectScreenLayoutPreviewNoUsers() {
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun UserComponent(
user: User,

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface
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.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun EpisodeCard(
episode: FindroidEpisode,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface
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.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun ItemCard(
item: FindroidItem,

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import coil.compose.AsyncImage
import dev.jdtech.jellyfin.models.FindroidEpisode
@ -17,7 +16,6 @@ enum class Direction {
HORIZONTAL, VERTICAL
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun ItemPoster(
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.width
import androidx.compose.ui.zIndex
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.TabRow
@ -35,7 +34,6 @@ import androidx.tv.material3.TabRow
*
* This component is adapted from androidx.tv.material3.TabRowDefaults.PillIndicator
*/
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun PillBorderIndicator(
currentTabPosition: DpRect,

View file

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

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
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.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun ProgressBadge(
item: FindroidItem,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
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.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun SettingsCategoryCard(
preference: PreferenceCategory,

View file

@ -22,7 +22,6 @@ import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.RadioButton
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.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun SettingsDetailsCard(
preference: PreferenceSelect,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
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.core.R as CoreR
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun SettingsSelectCard(
preference: PreferenceSelect,

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ClickableSurfaceScale
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
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.spacings
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun SettingsSwitchCard(
preference: PreferenceSwitch,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
@Immutable
@ -17,9 +16,7 @@ data class Spacings(
val extraLarge: Dp = 64.dp,
)
@OptIn(ExperimentalTvMaterial3Api::class)
val MaterialTheme.spacings
get() = Spacings()
@OptIn(ExperimentalTvMaterial3Api::class)
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.Color
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.SurfaceDefaults
import androidx.tv.material3.MaterialTheme as MaterialThemeTv
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun FindroidTheme(
content: @Composable BoxScope.() -> Unit,
@ -34,7 +32,7 @@ fun FindroidTheme(
shapes = shapesTv,
content = {
Surface(
colors = NonInteractiveSurfaceDefaults.colors(
colors = SurfaceDefaults.colors(
containerColor = androidx.tv.material3.MaterialTheme.colorScheme.background,
),
shape = RectangleShape,

View file

@ -4,7 +4,6 @@ import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Typography as TypographyTv
val Typography = Typography(
@ -34,7 +33,6 @@ val Typography = Typography(
),
)
@OptIn(ExperimentalTvMaterial3Api::class)
val TypographyTv = TypographyTv(
displayMedium = Typography.displayMedium,
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 {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
@ -19,17 +16,6 @@ allprojects {
google()
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") {

View file

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

View file

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

View file

@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.navigation.safeargs)
@ -29,7 +30,6 @@ android {
flavorDimensions += "variant"
productFlavors {
register("libre")
register("huawei")
}
compileOptions {
@ -40,10 +40,6 @@ android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.composeCompiler
}
}
ktlint {
@ -53,18 +49,18 @@ ktlint {
}
dependencies {
implementation(project(":data"))
implementation(project(":preferences"))
implementation(project(":player:core"))
implementation(libs.androidx.activity)
val composeBom = platform(libs.androidx.compose.bom)
implementation(projects.data)
implementation(projects.preferences)
implementation(projects.player.core)
implementation(libs.androidx.appcompat)
implementation(composeBom)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.core)
implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.paging)
implementation(libs.androidx.preference)
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
jellyfinApi.apply {
api.baseUrl = serverAddress.address
api.accessToken = user?.accessToken
api.update(
baseUrl = serverAddress.address,
accessToken = user?.accessToken,
)
userId = user?.id
}

View file

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

View file

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

View file

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

View file

@ -202,8 +202,10 @@ constructor(
appPreferences.currentServer = server.id
jellyfinApi.apply {
api.baseUrl = recommendedServerInfo.address
api.accessToken = null
api.update(
baseUrl = recommendedServerInfo.address,
accessToken = null,
)
}
_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.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.Dispatchers
@ -39,7 +40,10 @@ constructor(
_uiState.emit(UiState.Loading)
try {
val items = jellyfinRepository.getItems(parentId = parentId)
val items = jellyfinRepository.getItems(
parentId = parentId,
sortBy = SortBy.RELEASE_DATE,
)
if (items.isEmpty()) {
_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.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.toView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@ -41,13 +42,12 @@ class HomeViewModel @Inject internal constructor(
viewModelScope.launch {
try {
repository.postCapabilities()
} catch (_: Exception) {
}
} catch (_: Exception) { }
}
}
fun loadData() {
viewModelScope.launch {
viewModelScope.launch(Dispatchers.Default) {
_uiState.emit(UiState.Loading)
try {
val items = mutableListOf<HomeItem>()
@ -93,7 +93,7 @@ class HomeViewModel @Inject internal constructor(
private suspend fun loadViews() = repository
.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) }
.filter { (_, latest) -> latest.isNotEmpty() }
.map { (view, latest) -> view.toView().apply { items = latest } }

View file

@ -48,16 +48,20 @@ constructor(
CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
CollectionType.Mixed -> listOf(BaseItemKind.FOLDER, BaseItemKind.MOVIE, BaseItemKind.SERIES)
else -> null
}
val recursive = itemType == null || !itemType.contains(BaseItemKind.FOLDER)
viewModelScope.launch {
_uiState.emit(UiState.Loading)
try {
val items = jellyfinRepository.getItemsPaging(
parentId = parentId,
includeTypes = itemType,
recursive = true,
sortBy = sortBy,
recursive = recursive,
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,
).cachedIn(viewModelScope)
_uiState.emit(UiState.Normal(items))

View file

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

View file

@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.flow.MutableStateFlow
@ -35,9 +34,7 @@ constructor(
viewModelScope.launch {
_uiState.emit(UiState.Loading)
try {
val items = jellyfinRepository.getLibraries()
val collections =
items.filter { collection -> collection.type in CollectionType.supported }
val collections = jellyfinRepository.getLibraries()
_uiState.emit(UiState.Normal(collections))
} catch (e: Exception) {
_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.MediaStream
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.util.UUID
import javax.inject.Inject
@ -119,7 +121,7 @@ constructor(
private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> {
val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" }
actors = item.people.filter { it.type == PersonKind.ACTOR }
}
return actors
}
@ -127,7 +129,7 @@ constructor(
private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? {
val director: BaseItemPerson?
withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" }
director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
}
return director
}
@ -135,7 +137,7 @@ constructor(
private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> {
val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" }
writers = item.people.filter { it.type == PersonKind.WRITER }
}
return writers
}
@ -213,9 +215,9 @@ constructor(
DisplayProfile.DOLBY_VISION
} else {
when (videoRangeType) {
DisplayProfile.HDR.raw -> DisplayProfile.HDR
DisplayProfile.HDR10.raw -> DisplayProfile.HDR10
DisplayProfile.HLG.raw -> DisplayProfile.HLG
VideoRangeType.HDR10 -> DisplayProfile.HDR10
VideoRangeType.HDR10_PLUS -> DisplayProfile.HDR10_PLUS
VideoRangeType.HLG -> DisplayProfile.HLG
else -> DisplayProfile.SDR
}
},

View file

@ -1,5 +1,6 @@
package dev.jdtech.jellyfin.viewmodels
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -73,17 +74,29 @@ constructor(
server.currentServerAddressId = address.id
database.update(server)
jellyfinApi.api.baseUrl = address.address
jellyfinApi.api.update(
baseUrl = address.address,
)
eventsChannel.send(ServerAddressesEvent.NavigateToHome)
}
}
fun addAddress(address: String) {
fun addAddress(context: Context, address: String) {
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)
database.insertServerAddress(serverAddress)
loadAddresses(currentServerId)
} catch (_: Exception) { }
}
}
}

View file

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

View file

@ -97,7 +97,7 @@ constructor(
nameStringResource = R.string.pref_player_mpv_vo,
dependencies = listOf(Constants.PREF_PLAYER_MPV),
backendName = Constants.PREF_PLAYER_MPV_VO,
backendDefaultValue = "gpu",
backendDefaultValue = "gpu-next",
options = 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.withContext
import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.PersonKind
import java.util.UUID
import javax.inject.Inject
@ -100,7 +101,7 @@ constructor(
private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> {
val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" }
actors = item.people.filter { it.type == PersonKind.ACTOR }
}
return actors
}
@ -108,7 +109,7 @@ constructor(
private suspend fun getDirector(item: FindroidShow): BaseItemPerson? {
val director: BaseItemPerson?
withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" }
director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
}
return director
}
@ -116,7 +117,7 @@ constructor(
private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> {
val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" }
writers = item.people.filter { it.type == PersonKind.WRITER }
}
return writers
}

View file

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

View file

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

View file

@ -40,8 +40,10 @@ class SyncWorker @AssistedInject constructor(
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue
for (user in serverWithAddressesAndUsers.users) {
jellyfinApi.apply {
api.baseUrl = serverAddress.address
api.accessToken = user.accessToken
api.update(
baseUrl = serverAddress.address,
accessToken = user.accessToken,
)
userId = user.id
}
val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) }
@ -66,17 +68,16 @@ class SyncWorker @AssistedInject constructor(
try {
when (userData.played) {
true -> jellyfinApi.playStateApi.markPlayedItem(user.id, item.id)
false -> jellyfinApi.playStateApi.markUnplayedItem(user.id, item.id)
true -> jellyfinApi.playStateApi.markPlayedItem(item.id, user.id)
false -> jellyfinApi.playStateApi.markUnplayedItem(item.id, user.id)
}
when (userData.favorite) {
true -> jellyfinApi.userLibraryApi.markFavoriteItem(user.id, item.id)
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(user.id, item.id)
true -> jellyfinApi.userLibraryApi.markFavoriteItem(item.id, user.id)
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(item.id, user.id)
}
jellyfinApi.playStateApi.onPlaybackStopped(
userId = user.id,
itemId = item.id,
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="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="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="offline_mode">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_summary">Deslizar horizontalmente para buscar adelante o atrás</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="preparing_download">Preparando descarga</string>
<string name="cancel_download">Cancelar descarga</string>

View file

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

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