Merge branch 'jarnedemeulemeester:main' into auto-offline-mode
This commit is contained in:
commit
e74a86da24
245 changed files with 4344 additions and 1136 deletions
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
|
@ -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
65
.github/workflows/publish.yaml
vendored
Normal 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
6
.gitignore
vendored
|
@ -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
3
Gemfile
Normal file
|
@ -0,0 +1,3 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
218
Gemfile.lock
Normal file
218
Gemfile.lock
Normal 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
|
13
README.md
13
README.md
|
@ -1,6 +1,12 @@
|
|||

|
||||
|
||||
# Findroid
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private fun applyTheme() {
|
||||
if (appPreferences.amoledTheme) {
|
||||
setTheme(CoreR.style.Theme_FindroidAMOLED)
|
||||
setTheme(CoreR.style.ThemeOverlay_Findroid_Amoled)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
// 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)
|
||||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
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) {
|
||||
binding.playerView.useController = true
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
binding.communityRating.text = episode.communityRating.toString()
|
||||
episode.communityRating?.also {
|
||||
binding.communityRating.text = episode.communityRating.toString()
|
||||
binding.communityRating.isVisible = true
|
||||
}
|
||||
binding.missingIcon.isVisible = false
|
||||
|
||||
if (appPreferences.displayExtraInfo) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
binding.communityRating.text = item.communityRating.toString()
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -8,55 +8,16 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
<ImageButton
|
||||
android:id="@+id/btn_unlock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:contentDescription="@string/select_playback_speed"
|
||||
android:padding="16dp"
|
||||
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"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:contentDescription="@string/select_playback_speed"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_unlock"
|
||||
app:tint="@android:color/white" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:src="@drawable/ic_unlock"
|
||||
app:tint="@android:color/white" />
|
||||
|
||||
</FrameLayout>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
app/tv/proguard-rules.pro
vendored
2
app/tv/proguard-rules.pro
vendored
|
@ -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.**
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -55,6 +55,8 @@ val dummyEpisode = FindroidEpisode(
|
|||
seasonId = UUID.randomUUID(),
|
||||
communityRating = 9.2f,
|
||||
images = FindroidImages(),
|
||||
chapters = null,
|
||||
trickplayInfo = null,
|
||||
)
|
||||
|
||||
val dummyEpisodes = listOf(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") {
|
||||
|
|
1
buildSrc/settings.gradle.kts
Normal file
1
buildSrc/settings.gradle.kts
Normal file
|
@ -0,0 +1 @@
|
|||
rootProject.name = "buildSrc"
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) { _, _ ->
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -18,7 +18,7 @@ fun BaseItemDto.toView(): View {
|
|||
return View(
|
||||
id = id,
|
||||
name = name ?: "",
|
||||
type = CollectionType.fromString(collectionType),
|
||||
type = CollectionType.fromString(collectionType?.serialName),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
file.writeBytes(byteArray)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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 } }
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
val serverAddress = ServerAddress(UUID.randomUUID(), currentServerId, address)
|
||||
database.insertServerAddress(serverAddress)
|
||||
loadAddresses(currentServerId)
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,9 @@ constructor(
|
|||
database.update(server)
|
||||
|
||||
jellyfinApi.apply {
|
||||
api.accessToken = user.accessToken
|
||||
api.update(
|
||||
accessToken = user.accessToken,
|
||||
)
|
||||
userId = user.id
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
27
core/src/main/res/drawable/ic_speed_forward.xml
Normal file
27
core/src/main/res/drawable/ic_speed_forward.xml
Normal 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>
|
2
core/src/main/res/values-ar/strings.xml
Normal file
2
core/src/main/res/values-ar/strings.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue