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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew lintDebug ktlintCheck
|
run: ./gradlew lintDebug ktlintCheck
|
||||||
assemble:
|
assemble:
|
||||||
|
@ -29,14 +29,14 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/actions/wrapper-validation@v3
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew assembleDebug
|
run: ./gradlew assembleDebug
|
||||||
# Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged.
|
# Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged.
|
||||||
|
|
65
.github/workflows/publish.yaml
vendored
Normal file
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
|
# Android Profiling
|
||||||
*.hprof
|
*.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
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
Findroid is third-party Android application for Jellyfin that provides a native user interface to browse and play movies and series.
|
Findroid is third-party Android application for Jellyfin that provides a native user interface to browse and play movies and series.
|
||||||
|
|
||||||
|
@ -8,7 +14,7 @@ I am developing this application in my spare time.
|
||||||
|
|
||||||
**This project is in its early stages so expect bugs.**
|
**This project is in its early stages so expect bugs.**
|
||||||
|
|
||||||
<a href='https://play.google.com/store/apps/details?id=dev.jdtech.jellyfin'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height="80"/></a><a href='http://www.amazon.com/gp/product/B0BTWC8DNZ'><img alt='Available at Amazon Appstore' src='https://user-images.githubusercontent.com/32322857/219019331-027a6775-7362-44bb-a026-281f71e9b37b.png' height="80"/></a><a href='https://appgallery.huawei.com/app/C107646987'><img alt='Explore it on Huawei AppGallery' src='https://user-images.githubusercontent.com/32322857/219013519-f0a11e17-c32c-42fd-8009-ab77fe2c23e7.png' height="80"/></a><a href='https://apt.izzysoft.de/fdroid/index/apk/dev.jdtech.jellyfin'><img alt='Get it on IzzyOnDroid' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' height="80"/></a>
|
<a href='https://play.google.com/store/apps/details?id=dev.jdtech.jellyfin'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height="80"/></a><a href='http://www.amazon.com/gp/product/B0BTWC8DNZ'><img alt='Available at Amazon Appstore' src='https://user-images.githubusercontent.com/32322857/219019331-027a6775-7362-44bb-a026-281f71e9b37b.png' height="80"/></a><a href='https://apt.izzysoft.de/fdroid/index/apk/dev.jdtech.jellyfin'><img alt='Get it on IzzyOnDroid' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' height="80"/></a>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
| Home | Library | Movie | Season | Episode |
|
| Home | Library | Movie | Season | Episode |
|
||||||
|
@ -23,7 +29,7 @@ I am developing this application in my spare time.
|
||||||
- ExoPlayer
|
- ExoPlayer
|
||||||
- Video codecs: H.263, H.264, H.265, VP8, VP9, AV1
|
- Video codecs: H.263, H.264, H.265, VP8, VP9, AV1
|
||||||
- Support depends on Android device
|
- Support depends on Android device
|
||||||
- Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AMR-NB, AMR-WB, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD
|
- Audio codecs: Vorbis, Opus, FLAC, ALAC, PCM, MP3, AAC, AC-3, E-AC-3, DTS, DTS-HD, TrueHD
|
||||||
- Support provided by ExoPlayer FFmpeg extension
|
- Support provided by ExoPlayer FFmpeg extension
|
||||||
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
|
- Subtitle codecs: SRT, VTT, SSA/ASS, PGSSUB
|
||||||
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435)
|
- SSA/ASS has limited styling support see [this issue](https://github.com/google/ExoPlayer/issues/8435)
|
||||||
|
@ -34,6 +40,9 @@ I am developing this application in my spare time.
|
||||||
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
|
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
|
||||||
- Optionally force software decoding when hardware decoding has issues.
|
- Optionally force software decoding when hardware decoding has issues.
|
||||||
- Picture-in-picture mode
|
- Picture-in-picture mode
|
||||||
|
- Media chapters
|
||||||
|
- Timeline markers
|
||||||
|
- Chapter navigation gestures
|
||||||
|
|
||||||
## Planned features
|
## Planned features
|
||||||
- Android TV
|
- Android TV
|
||||||
|
|
|
@ -21,6 +21,20 @@ android {
|
||||||
|
|
||||||
versionCode = Versions.appCode
|
versionCode = Versions.appCode
|
||||||
versionName = Versions.appName
|
versionName = Versions.appName
|
||||||
|
|
||||||
|
testInstrumentationRunner = "dev.jdtech.jellyfin.HiltTestRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
val variant = this
|
||||||
|
variant.outputs
|
||||||
|
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||||
|
.forEach { output ->
|
||||||
|
if (variant.buildType.name == "release") {
|
||||||
|
val outputFileName = "findroid-v${variant.versionName}-${variant.flavorName}-${output.getFilter("ABI")}.apk"
|
||||||
|
output.outputFileName = outputFileName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -47,9 +61,6 @@ android {
|
||||||
dimension = "variant"
|
dimension = "variant"
|
||||||
isDefault = true
|
isDefault = true
|
||||||
}
|
}
|
||||||
register("huawei") {
|
|
||||||
dimension = "variant"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
|
@ -61,6 +72,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = Versions.java
|
sourceCompatibility = Versions.java
|
||||||
targetCompatibility = Versions.java
|
targetCompatibility = Versions.java
|
||||||
}
|
}
|
||||||
|
@ -78,11 +91,11 @@ ktlint {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core"))
|
implementation(projects.core)
|
||||||
implementation(project(":data"))
|
implementation(projects.data)
|
||||||
implementation(project(":preferences"))
|
implementation(projects.preferences)
|
||||||
implementation(project(":player:core"))
|
implementation(projects.player.core)
|
||||||
implementation(project(":player:video"))
|
implementation(projects.player.video)
|
||||||
implementation(libs.aboutlibraries.core)
|
implementation(libs.aboutlibraries.core)
|
||||||
implementation(libs.aboutlibraries)
|
implementation(libs.aboutlibraries)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
|
@ -90,9 +103,7 @@ dependencies {
|
||||||
implementation(libs.androidx.constraintlayout)
|
implementation(libs.androidx.constraintlayout)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.hilt.work)
|
implementation(libs.androidx.hilt.work)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
implementation(libs.androidx.media3.exoplayer)
|
|
||||||
implementation(libs.androidx.media3.ui)
|
implementation(libs.androidx.media3.ui)
|
||||||
implementation(libs.androidx.media3.session)
|
implementation(libs.androidx.media3.session)
|
||||||
implementation(libs.androidx.navigation.fragment)
|
implementation(libs.androidx.navigation.fragment)
|
||||||
|
@ -109,7 +120,14 @@ dependencies {
|
||||||
implementation(libs.jellyfin.core)
|
implementation(libs.jellyfin.core)
|
||||||
compileOnly(libs.libmpv)
|
compileOnly(libs.libmpv)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
implementation(libs.media3.ffmpeg.decoder)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
implementation(rootProject.files("libs/lib-decoder-ffmpeg-release.aar"))
|
coreLibraryDesugaring(libs.android.desugar.jdk)
|
||||||
|
|
||||||
|
androidTestImplementation(libs.androidx.room.runtime)
|
||||||
|
androidTestImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.bundles.androidx.test)
|
||||||
|
androidTestImplementation(libs.hilt.android.testing)
|
||||||
|
kspTest(libs.hilt.android.compiler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.disk.DiskCache
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactory {
|
class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactory {
|
||||||
|
@ -40,7 +42,12 @@ class BaseApplication : Application(), Configuration.Provider, ImageLoaderFactor
|
||||||
"dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
"dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appPreferences.dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this)
|
if (appPreferences.dynamicColors) {
|
||||||
|
val dynamicColorsOptions = DynamicColorsOptions.Builder()
|
||||||
|
.setThemeOverlay(CoreR.style.ThemeOverlay_Findroid_DynamicColors)
|
||||||
|
.build()
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(this, dynamicColorsOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.media3.exoplayer.trackselection.MappingTrackSelector
|
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
|
||||||
|
@ -72,19 +71,6 @@ abstract class BasePlayerActivity : AppCompatActivity() {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun isRendererType(
|
|
||||||
mappedTrackInfo: MappingTrackSelector.MappedTrackInfo,
|
|
||||||
rendererIndex: Int,
|
|
||||||
type: Int,
|
|
||||||
): Boolean {
|
|
||||||
val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
|
|
||||||
if (trackGroupArray.length == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val trackType = mappedTrackInfo.getRendererType(rendererIndex)
|
|
||||||
return type == trackType
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun configureInsets(playerControls: View) {
|
protected fun configureInsets(playerControls: View) {
|
||||||
playerControls.setOnApplyWindowInsetsListener { _, windowInsets ->
|
playerControls.setOnApplyWindowInsetsListener { _, windowInsets ->
|
||||||
val cutout = windowInsets.displayCutout
|
val cutout = windowInsets.displayCutout
|
||||||
|
|
|
@ -165,7 +165,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun applyTheme() {
|
private fun applyTheme() {
|
||||||
if (appPreferences.amoledTheme) {
|
if (appPreferences.amoledTheme) {
|
||||||
setTheme(CoreR.style.Theme_FindroidAMOLED)
|
setTheme(CoreR.style.ThemeOverlay_Findroid_Amoled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,13 @@ import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
@ -27,15 +29,14 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
|
import androidx.media3.ui.PlayerControlView
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
|
||||||
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
||||||
import dev.jdtech.jellyfin.utils.PreviewScrubListener
|
import dev.jdtech.jellyfin.utils.PreviewScrubListener
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
@ -56,6 +57,7 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
private var playerGestureHelper: PlayerGestureHelper? = null
|
private var playerGestureHelper: PlayerGestureHelper? = null
|
||||||
override val viewModel: PlayerActivityViewModel by viewModels()
|
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||||
private var previewScrubListener: PreviewScrubListener? = null
|
private var previewScrubListener: PreviewScrubListener? = null
|
||||||
|
private var wasZoom: Boolean = false
|
||||||
|
|
||||||
private val isPipSupported by lazy {
|
private val isPipSupported by lazy {
|
||||||
// Check if device has PiP feature
|
// Check if device has PiP feature
|
||||||
|
@ -112,10 +114,6 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playerView.findViewById<View>(R.id.back_button_alt).setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
|
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
|
||||||
|
|
||||||
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
||||||
|
@ -143,9 +141,21 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trick Play
|
// Trickplay
|
||||||
previewScrubListener?.let {
|
previewScrubListener?.let {
|
||||||
it.currentTrickPlay = currentTrickPlay
|
it.currentTrickplay = currentTrickplay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
if (appPreferences.showChapterMarkers && currentChapters != null) {
|
||||||
|
currentChapters?.let { chapters ->
|
||||||
|
val playerControlView = findViewById<PlayerControlView>(R.id.exo_controller)
|
||||||
|
val numOfChapters = chapters.size
|
||||||
|
playerControlView.setExtraAdGroupMarkers(
|
||||||
|
LongArray(numOfChapters) { index -> chapters[index].startPosition },
|
||||||
|
BooleanArray(numOfChapters) { false },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File Loaded
|
// File Loaded
|
||||||
|
@ -169,6 +179,13 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
viewModel.eventsChannelFlow.collect { event ->
|
viewModel.eventsChannelFlow.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is PlayerEvents.NavigateBack -> finish()
|
is PlayerEvents.NavigateBack -> finish()
|
||||||
|
is PlayerEvents.IsPlayingChanged -> {
|
||||||
|
if (appPreferences.playerPipGesture) {
|
||||||
|
try {
|
||||||
|
setPictureInPictureParams(pipParams(event.isPlaying))
|
||||||
|
} catch (_: IllegalArgumentException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,9 +255,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
pictureInPicture()
|
pictureInPicture()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appPreferences.playerTrickPlay) {
|
// Set marker color
|
||||||
|
val 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 imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
|
||||||
previewScrubListener = PreviewScrubListener(
|
previewScrubListener = PreviewScrubListener(
|
||||||
imagePreview,
|
imagePreview,
|
||||||
timeBar,
|
timeBar,
|
||||||
|
@ -254,7 +274,7 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
hideSystemUI()
|
hideSystemUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
|
|
||||||
|
@ -263,12 +283,17 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
override fun onUserLeaveHint() {
|
||||||
if (appPreferences.playerPipGesture && viewModel.player.isPlaying && !isControlsLocked) {
|
super.onUserLeaveHint()
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
|
||||||
|
appPreferences.playerPipGesture &&
|
||||||
|
viewModel.player.isPlaying &&
|
||||||
|
!isControlsLocked
|
||||||
|
) {
|
||||||
pictureInPicture()
|
pictureInPicture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pipParams(): PictureInPictureParams {
|
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
|
||||||
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
||||||
|
|
||||||
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
||||||
|
@ -296,24 +321,21 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PictureInPictureParams.Builder()
|
val builder = PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(aspectRatio)
|
.setAspectRatio(aspectRatio)
|
||||||
.setSourceRectHint(sourceRectHint)
|
.setSourceRectHint(sourceRectHint)
|
||||||
.build()
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
builder.setAutoEnterEnabled(enableAutoEnter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pictureInPicture() {
|
private fun pictureInPicture() {
|
||||||
if (!isPipSupported) {
|
if (!isPipSupported) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
binding.playerView.useController = false
|
|
||||||
binding.playerView.findViewById<Button>(R.id.btn_skip_intro).isVisible = false
|
|
||||||
|
|
||||||
if (binding.playerView.player is MPVPlayer) {
|
|
||||||
(binding.playerView.player as MPVPlayer).updateZoomMode(false)
|
|
||||||
} else {
|
|
||||||
binding.playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enterPictureInPictureMode(pipParams())
|
enterPictureInPictureMode(pipParams())
|
||||||
|
@ -325,8 +347,35 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
newConfig: Configuration,
|
newConfig: Configuration,
|
||||||
) {
|
) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
if (!isInPictureInPictureMode) {
|
when (isInPictureInPictureMode) {
|
||||||
binding.playerView.useController = true
|
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
|
package dev.jdtech.jellyfin.adapters
|
||||||
|
|
||||||
|
import android.text.Html.fromHtml
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -45,7 +46,7 @@ class EpisodeListAdapter(
|
||||||
binding.root.context.getString(CoreR.string.episode_name_with_end, episode.indexNumber, episode.indexNumberEnd, episode.name)
|
binding.root.context.getString(CoreR.string.episode_name_with_end, episode.indexNumber, episode.indexNumberEnd, episode.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.episodeOverview.text = episode.overview
|
binding.episodeOverview.text = fromHtml(episode.overview, 0)
|
||||||
|
|
||||||
if (episode.playbackPositionTicks > 0) {
|
if (episode.playbackPositionTicks > 0) {
|
||||||
binding.progressBar.layoutParams.width = TypedValue.applyDimension(
|
binding.progressBar.layoutParams.width = TypedValue.applyDimension(
|
||||||
|
|
|
@ -24,6 +24,7 @@ import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.CollectionViewModel
|
import dev.jdtech.jellyfin.viewmodels.CollectionViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class CollectionFragment : Fragment() {
|
class CollectionFragment : Fragment() {
|
||||||
|
@ -40,6 +41,8 @@ class CollectionFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
|
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
binding.noFavoritesText.text = getString(CoreR.string.collection_no_media)
|
||||||
|
|
||||||
binding.favoritesRecyclerView.adapter = FavoritesListAdapter { item ->
|
binding.favoritesRecyclerView.adapter = FavoritesListAdapter { item ->
|
||||||
navigateToMediaItem(item)
|
navigateToMediaItem(item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html.fromHtml
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -285,11 +286,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.seriesName.text = episode.seriesName
|
binding.seriesName.text = episode.seriesName
|
||||||
binding.overview.text = episode.overview
|
binding.overview.text = fromHtml(episode.overview, 0)
|
||||||
binding.year.text = formatDateTime(episode.premiereDate)
|
binding.year.text = formatDateTime(episode.premiereDate)
|
||||||
binding.playtime.text = getString(CoreR.string.runtime_minutes, episode.runtimeTicks.div(600000000))
|
binding.playtime.text = getString(CoreR.string.runtime_minutes, episode.runtimeTicks.div(600000000))
|
||||||
binding.communityRating.isVisible = episode.communityRating != null
|
episode.communityRating?.also {
|
||||||
binding.communityRating.text = episode.communityRating.toString()
|
binding.communityRating.text = episode.communityRating.toString()
|
||||||
|
binding.communityRating.isVisible = true
|
||||||
|
}
|
||||||
binding.missingIcon.isVisible = false
|
binding.missingIcon.isVisible = false
|
||||||
|
|
||||||
if (appPreferences.displayExtraInfo) {
|
if (appPreferences.displayExtraInfo) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.FindroidBoxSet
|
import dev.jdtech.jellyfin.models.FindroidBoxSet
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidFolder
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
|
@ -222,6 +223,15 @@ class LibraryFragment : Fragment() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is FindroidFolder -> {
|
||||||
|
findNavController().navigate(
|
||||||
|
LibraryFragmentDirections.actionLibraryFragmentSelf(
|
||||||
|
item.id,
|
||||||
|
item.name,
|
||||||
|
args.libraryType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.fragments
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html.fromHtml
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -82,7 +83,7 @@ class LoginFragment : Fragment() {
|
||||||
viewModel.uiState.collect { uiState ->
|
viewModel.uiState.collect { uiState ->
|
||||||
Timber.d("$uiState")
|
Timber.d("$uiState")
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
is LoginViewModel.UiState.Normal -> bindUiStateNormal()
|
is LoginViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
|
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
|
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
}
|
}
|
||||||
|
@ -135,11 +136,15 @@ class LoginFragment : Fragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateNormal() {
|
private fun bindUiStateNormal(uiState: LoginViewModel.UiState.Normal) {
|
||||||
binding.buttonLogin.isEnabled = true
|
binding.buttonLogin.isEnabled = true
|
||||||
binding.progressCircular.isVisible = false
|
binding.progressCircular.isVisible = false
|
||||||
binding.editTextUsernameLayout.isEnabled = true
|
binding.editTextUsernameLayout.isEnabled = true
|
||||||
binding.editTextPasswordLayout.isEnabled = true
|
binding.editTextPasswordLayout.isEnabled = true
|
||||||
|
|
||||||
|
uiState.disclaimer?.let { disclaimer ->
|
||||||
|
binding.loginDisclaimer.text = fromHtml(disclaimer, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
|
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.app.DownloadManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html.fromHtml
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -276,7 +277,6 @@ class MovieFragment : Fragment() {
|
||||||
if (item.trailer != null) {
|
if (item.trailer != null) {
|
||||||
binding.itemActions.trailerButton.isVisible = true
|
binding.itemActions.trailerButton.isVisible = true
|
||||||
}
|
}
|
||||||
binding.communityRating.isVisible = item.communityRating != null
|
|
||||||
binding.actors.isVisible = actors.isNotEmpty()
|
binding.actors.isVisible = actors.isNotEmpty()
|
||||||
|
|
||||||
binding.itemActions.playButton.isEnabled = item.canPlay && item.sources.isNotEmpty()
|
binding.itemActions.playButton.isEnabled = item.canPlay && item.sources.isNotEmpty()
|
||||||
|
@ -309,7 +309,10 @@ class MovieFragment : Fragment() {
|
||||||
binding.playtime.text = runTime
|
binding.playtime.text = runTime
|
||||||
}
|
}
|
||||||
binding.officialRating.text = item.officialRating
|
binding.officialRating.text = item.officialRating
|
||||||
binding.communityRating.text = item.communityRating.toString()
|
item.communityRating?.also {
|
||||||
|
binding.communityRating.text = it.toString()
|
||||||
|
binding.communityRating.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
videoMetadata.let {
|
videoMetadata.let {
|
||||||
with(binding) {
|
with(binding) {
|
||||||
|
@ -322,8 +325,8 @@ class MovieFragment : Fragment() {
|
||||||
it.displayProfiles.firstOrNull()?.apply {
|
it.displayProfiles.firstOrNull()?.apply {
|
||||||
videoProfileChip.text = this.raw
|
videoProfileChip.text = this.raw
|
||||||
videoProfileChip.isVisible = when (this) {
|
videoProfileChip.isVisible = when (this) {
|
||||||
DisplayProfile.HDR,
|
|
||||||
DisplayProfile.HDR10,
|
DisplayProfile.HDR10,
|
||||||
|
DisplayProfile.HDR10_PLUS,
|
||||||
DisplayProfile.HLG,
|
DisplayProfile.HLG,
|
||||||
-> {
|
-> {
|
||||||
videoProfileChip.chipStartPadding = .0f
|
videoProfileChip.chipStartPadding = .0f
|
||||||
|
@ -379,7 +382,7 @@ class MovieFragment : Fragment() {
|
||||||
binding.info.sizeGroup.isVisible = size != null
|
binding.info.sizeGroup.isVisible = size != null
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.info.description.text = item.overview
|
binding.info.description.text = fromHtml(item.overview, 0)
|
||||||
binding.info.genres.text = genresString
|
binding.info.genres.text = genresString
|
||||||
binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
|
binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
|
||||||
binding.info.director.text = director?.name
|
binding.info.director.text = director?.name
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.fragments
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Html.fromHtml
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -170,7 +171,6 @@ class ShowFragment : Fragment() {
|
||||||
if (item.trailer != null) {
|
if (item.trailer != null) {
|
||||||
binding.itemActions.trailerButton.isVisible = true
|
binding.itemActions.trailerButton.isVisible = true
|
||||||
}
|
}
|
||||||
binding.communityRating.isVisible = item.communityRating != null
|
|
||||||
binding.actors.isVisible = actors.isNotEmpty()
|
binding.actors.isVisible = actors.isNotEmpty()
|
||||||
|
|
||||||
// TODO currently the sources of a show is always empty, we need a way to check if sources are available
|
// TODO currently the sources of a show is always empty, we need a way to check if sources are available
|
||||||
|
@ -212,9 +212,12 @@ class ShowFragment : Fragment() {
|
||||||
binding.playtime.text = runTime
|
binding.playtime.text = runTime
|
||||||
}
|
}
|
||||||
binding.officialRating.text = item.officialRating
|
binding.officialRating.text = item.officialRating
|
||||||
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.genres.text = genresString
|
||||||
binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
|
binding.info.genresGroup.isVisible = item.genres.isNotEmpty()
|
||||||
binding.info.director.text = director?.name
|
binding.info.director.text = director?.name
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.Constants
|
import dev.jdtech.jellyfin.Constants
|
||||||
import dev.jdtech.jellyfin.PlayerActivity
|
import dev.jdtech.jellyfin.PlayerActivity
|
||||||
import dev.jdtech.jellyfin.isControlsLocked
|
import dev.jdtech.jellyfin.isControlsLocked
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -37,7 +38,7 @@ class PlayerGestureHelper(
|
||||||
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
|
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
|
||||||
* Useful on wide-screen phones to remove black bars from some movies.
|
* Useful on wide-screen phones to remove black bars from some movies.
|
||||||
*/
|
*/
|
||||||
private var isZoomEnabled = false
|
var isZoomEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
||||||
|
@ -55,9 +56,14 @@ class PlayerGestureHelper(
|
||||||
|
|
||||||
private var lastScaleEvent: Long = 0
|
private var lastScaleEvent: Long = 0
|
||||||
|
|
||||||
|
private var playbackSpeedIncrease: Float = 2f
|
||||||
|
private var lastPlaybackSpeed: Float = 0f
|
||||||
|
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
|
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
|
||||||
|
|
||||||
|
private var currentNumberOfPointers: Int = 0
|
||||||
|
|
||||||
private val tapGestureDetector = GestureDetector(
|
private val tapGestureDetector = GestureDetector(
|
||||||
playerView.context,
|
playerView.context,
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@ -69,6 +75,22 @@ class PlayerGestureHelper(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent) {
|
||||||
|
// Disables long press gesture if view is locked
|
||||||
|
if (isControlsLocked) return
|
||||||
|
|
||||||
|
// Stop long press gesture when more than 1 pointer
|
||||||
|
if (currentNumberOfPointers > 1) return
|
||||||
|
|
||||||
|
// This is a temporary solution for chapter skipping.
|
||||||
|
// TODO: Remove this after implementing #636
|
||||||
|
if (appPreferences.playerGesturesChapterSkip) {
|
||||||
|
handleChapterSkip(e)
|
||||||
|
} else {
|
||||||
|
enableSpeedIncrease()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
// Disables double tap gestures if view is locked
|
// Disables double tap gestures if view is locked
|
||||||
if (isControlsLocked) return false
|
if (isControlsLocked) return false
|
||||||
|
@ -100,6 +122,55 @@ class PlayerGestureHelper(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun enableSpeedIncrease() {
|
||||||
|
playerView.player?.let {
|
||||||
|
if (it.isPlaying) {
|
||||||
|
lastPlaybackSpeed = it.playbackParameters.speed
|
||||||
|
it.setPlaybackSpeed(playbackSpeedIncrease)
|
||||||
|
activity.binding.gestureSpeedText.text = playbackSpeedIncrease.toString() + "x"
|
||||||
|
activity.binding.gestureSpeedLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleChapterSkip(e: MotionEvent) {
|
||||||
|
if (isControlsLocked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewWidth = playerView.measuredWidth
|
||||||
|
val areaWidth = viewWidth / 5 // Divide the view into 5 parts: 2:1:2
|
||||||
|
|
||||||
|
// Define the areas and their boundaries
|
||||||
|
val leftmostAreaStart = 0
|
||||||
|
val middleAreaStart = areaWidth * 2
|
||||||
|
val rightmostAreaStart = middleAreaStart + areaWidth
|
||||||
|
|
||||||
|
when (e.x.toInt()) {
|
||||||
|
in leftmostAreaStart until middleAreaStart -> {
|
||||||
|
activity.viewModel.seekToPreviousChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in rightmostAreaStart until viewWidth -> {
|
||||||
|
if (activity.viewModel.isLastChapter() == true) {
|
||||||
|
playerView.player?.seekToNextMediaItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity.viewModel.seekToNextChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayChapter(chapter: PlayerChapter) {
|
||||||
|
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
|
||||||
|
activity.binding.progressScrubberText.text = chapter.name ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
private fun fastForward() {
|
private fun fastForward() {
|
||||||
val currentPosition = playerView.player?.currentPosition ?: 0
|
val currentPosition = playerView.player?.currentPosition ?: 0
|
||||||
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
||||||
|
@ -315,8 +386,8 @@ class PlayerGestureHelper(
|
||||||
lastScaleEvent = SystemClock.elapsedRealtime()
|
lastScaleEvent = SystemClock.elapsedRealtime()
|
||||||
val scaleFactor = detector.scaleFactor
|
val scaleFactor = detector.scaleFactor
|
||||||
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
||||||
isZoomEnabled = scaleFactor > 1
|
val enableZoom = scaleFactor > 1
|
||||||
updateZoomMode(isZoomEnabled)
|
updateZoomMode(enableZoom)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -325,16 +396,17 @@ class PlayerGestureHelper(
|
||||||
},
|
},
|
||||||
).apply { isQuickScaleEnabled = false }
|
).apply { isQuickScaleEnabled = false }
|
||||||
|
|
||||||
private fun updateZoomMode(enabled: Boolean) {
|
fun updateZoomMode(enabled: Boolean) {
|
||||||
if (playerView.player is MPVPlayer) {
|
if (playerView.player is MPVPlayer) {
|
||||||
(playerView.player as MPVPlayer).updateZoomMode(enabled)
|
(playerView.player as MPVPlayer).updateZoomMode(enabled)
|
||||||
} else {
|
} else {
|
||||||
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
isZoomEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseAction(event: MotionEvent) {
|
private fun releaseAction(event: MotionEvent) {
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||||
activity.binding.gestureVolumeLayout.apply {
|
activity.binding.gestureVolumeLayout.apply {
|
||||||
if (visibility == View.VISIBLE) {
|
if (visibility == View.VISIBLE) {
|
||||||
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
|
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
|
||||||
|
@ -361,6 +433,12 @@ class PlayerGestureHelper(
|
||||||
swipeGestureValueTrackerProgress = -1L
|
swipeGestureValueTrackerProgress = -1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentNumberOfPointers = 0
|
||||||
|
}
|
||||||
|
if (lastPlaybackSpeed > 0 && (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL)) {
|
||||||
|
playerView.player?.setPlaybackSpeed(lastPlaybackSpeed)
|
||||||
|
lastPlaybackSpeed = 0f
|
||||||
|
activity.binding.gestureSpeedLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,9 +476,12 @@ class PlayerGestureHelper(
|
||||||
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
|
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateZoomMode(appPreferences.playerStartMaximized)
|
||||||
|
|
||||||
@Suppress("ClickableViewAccessibility")
|
@Suppress("ClickableViewAccessibility")
|
||||||
playerView.setOnTouchListener { _, event ->
|
playerView.setOnTouchListener { _, event ->
|
||||||
if (playerView.useController) {
|
if (playerView.useController) {
|
||||||
|
currentNumberOfPointers = event.pointerCount
|
||||||
when (event.pointerCount) {
|
when (event.pointerCount) {
|
||||||
1 -> {
|
1 -> {
|
||||||
tapGestureDetector.onTouchEvent(event)
|
tapGestureDetector.onTouchEvent(event)
|
||||||
|
|
|
@ -8,8 +8,8 @@ import androidx.media3.common.Player
|
||||||
import androidx.media3.ui.TimeBar
|
import androidx.media3.ui.TimeBar
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
import dev.jdtech.jellyfin.utils.bif.BifData
|
import dev.jdtech.jellyfin.models.Trickplay
|
||||||
import dev.jdtech.jellyfin.utils.bif.BifUtil
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class PreviewScrubListener(
|
class PreviewScrubListener(
|
||||||
|
@ -17,14 +17,14 @@ class PreviewScrubListener(
|
||||||
private val timeBarView: View,
|
private val timeBarView: View,
|
||||||
private val player: Player,
|
private val player: Player,
|
||||||
) : TimeBar.OnScrubListener {
|
) : TimeBar.OnScrubListener {
|
||||||
var currentTrickPlay: BifData? = null
|
var currentTrickplay: Trickplay? = null
|
||||||
private val roundedCorners = RoundedCornersTransformation(10f)
|
private val roundedCorners = RoundedCornersTransformation(10f)
|
||||||
private var currentBitMap: Bitmap? = null
|
private var currentBitMap: Bitmap? = null
|
||||||
|
|
||||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||||
Timber.d("Scrubbing started at $position")
|
Timber.d("Scrubbing started at $position")
|
||||||
|
|
||||||
if (currentTrickPlay == null) {
|
if (currentTrickplay == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,8 @@ class PreviewScrubListener(
|
||||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||||
Timber.d("Scrubbing to $position")
|
Timber.d("Scrubbing to $position")
|
||||||
|
|
||||||
val currentBifData = currentTrickPlay ?: return
|
val trickplay = currentTrickplay ?: return
|
||||||
val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return
|
val image = trickplay.images[position.div(trickplay.interval).toInt()]
|
||||||
|
|
||||||
val parent = scrubbingPreview.parent as ViewGroup
|
val parent = scrubbingPreview.parent as ViewGroup
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ class PreviewScrubListener(
|
||||||
|
|
||||||
if (currentBitMap != image) {
|
if (currentBitMap != image) {
|
||||||
scrubbingPreview.load(image) {
|
scrubbingPreview.load(image) {
|
||||||
|
dispatcher(Dispatchers.Main.immediate)
|
||||||
transformations(roundedCorners)
|
transformations(roundedCorners)
|
||||||
}
|
}
|
||||||
currentBitMap = image
|
currentBitMap = image
|
||||||
|
|
|
@ -113,6 +113,37 @@
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/gesture_speed_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_gravity="center_horizontal|top"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="@drawable/overlay_background"
|
||||||
|
android:clickable="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/gesture_speed_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/gesture_speed_image"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:src="@drawable/ic_speed_forward"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_ffwd_animation_ripple"
|
android:id="@+id/image_ffwd_animation_ripple"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
|
|
|
@ -8,55 +8,16 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ImageButton
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/btn_unlock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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:layout_margin="8dp"
|
||||||
android:orientation="horizontal">
|
android:src="@drawable/ic_unlock"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
<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>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/exo_controller">
|
||||||
|
|
||||||
<androidx.media3.ui.AspectRatioFrameLayout
|
<androidx.media3.ui.AspectRatioFrameLayout
|
||||||
android:id="@id/exo_content_frame"
|
android:id="@id/exo_content_frame"
|
||||||
|
@ -82,9 +83,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<View
|
<androidx.media3.ui.PlayerControlView
|
||||||
android:id="@id/exo_controller_placeholder"
|
android:id="@id/exo_controller"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
app:animation_enabled="false"/>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
|
@ -141,6 +141,15 @@
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/login_disclaimer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="Sample login disclaimer" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -123,6 +123,9 @@
|
||||||
<argument
|
<argument
|
||||||
android:name="libraryType"
|
android:name="libraryType"
|
||||||
app:argType="dev.jdtech.jellyfin.models.CollectionType" />
|
app:argType="dev.jdtech.jellyfin.models.CollectionType" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_libraryFragment_self"
|
||||||
|
app:destination="@id/libraryFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/showFragment"
|
android:id="@+id/showFragment"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose.compiler)
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.hilt)
|
alias(libs.plugins.hilt)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
|
@ -56,6 +57,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = Versions.java
|
sourceCompatibility = Versions.java
|
||||||
targetCompatibility = Versions.java
|
targetCompatibility = Versions.java
|
||||||
}
|
}
|
||||||
|
@ -64,10 +67,6 @@ android {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
@ -82,22 +81,26 @@ ktlint {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core"))
|
val composeBom = platform(libs.androidx.compose.bom)
|
||||||
implementation(project(":data"))
|
|
||||||
implementation(project(":preferences"))
|
implementation(projects.core)
|
||||||
implementation(project(":player:core"))
|
implementation(projects.data)
|
||||||
implementation(project(":player:video"))
|
implementation(projects.preferences)
|
||||||
|
implementation(projects.player.core)
|
||||||
|
implementation(projects.player.video)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(composeBom)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.media3.exoplayer)
|
implementation(libs.androidx.media3.exoplayer)
|
||||||
implementation(libs.androidx.media3.ui)
|
implementation(libs.androidx.media3.ui)
|
||||||
implementation(libs.androidx.media3.session)
|
implementation(libs.androidx.media3.session)
|
||||||
implementation(libs.androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
implementation(libs.androidx.tv.foundation)
|
||||||
|
implementation(libs.androidx.tv.material)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.coil.svg)
|
implementation(libs.coil.svg)
|
||||||
implementation(libs.compose.destinations.core)
|
implementation(libs.compose.destinations.core)
|
||||||
|
@ -105,8 +108,9 @@ dependencies {
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
implementation(libs.jellyfin.core)
|
implementation(libs.jellyfin.core)
|
||||||
implementation(libs.androidx.tv.foundation)
|
implementation(libs.media3.ffmpeg.decoder)
|
||||||
implementation(libs.androidx.tv.material)
|
|
||||||
|
coreLibraryDesugaring(libs.android.desugar.jdk)
|
||||||
|
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
}
|
}
|
||||||
|
|
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.SSLParameters
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||||
|
|
||||||
|
-keep class dev.jdtech.**
|
|
@ -23,7 +23,6 @@ data class PlayerActivityNavArgs(
|
||||||
@ActivityDestination(
|
@ActivityDestination(
|
||||||
navArgsDelegate = PlayerActivityNavArgs::class,
|
navArgsDelegate = PlayerActivityNavArgs::class,
|
||||||
)
|
)
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
||||||
class PlayerActivity : ComponentActivity() {
|
class PlayerActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
|
@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
@ -71,7 +70,6 @@ fun AddServerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AddServerScreenLayout(
|
private fun AddServerScreenLayout(
|
||||||
uiState: AddServerViewModel.UiState,
|
uiState: AddServerViewModel.UiState,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyRow
|
import androidx.tv.foundation.lazy.list.TvLazyRow
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
@ -88,7 +87,6 @@ fun HomeScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeScreenLayout(
|
private fun HomeScreenLayout(
|
||||||
uiState: HomeViewModel.UiState,
|
uiState: HomeViewModel.UiState,
|
||||||
|
|
|
@ -17,7 +17,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.grid.TvGridCells
|
import androidx.tv.foundation.lazy.grid.TvGridCells
|
||||||
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
||||||
import androidx.tv.foundation.lazy.grid.items
|
import androidx.tv.foundation.lazy.grid.items
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
@ -50,7 +49,6 @@ fun LibrariesScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibrariesScreenLayout(
|
private fun LibrariesScreenLayout(
|
||||||
uiState: MediaViewModel.UiState,
|
uiState: MediaViewModel.UiState,
|
||||||
|
|
|
@ -18,14 +18,15 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.tv.foundation.lazy.grid.TvGridCells
|
import androidx.tv.foundation.lazy.grid.TvGridCells
|
||||||
import androidx.tv.foundation.lazy.grid.TvGridItemSpan
|
import androidx.tv.foundation.lazy.grid.TvGridItemSpan
|
||||||
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import dev.jdtech.jellyfin.destinations.LibraryScreenDestination
|
||||||
import dev.jdtech.jellyfin.destinations.MovieScreenDestination
|
import dev.jdtech.jellyfin.destinations.MovieScreenDestination
|
||||||
import dev.jdtech.jellyfin.destinations.ShowScreenDestination
|
import dev.jdtech.jellyfin.destinations.ShowScreenDestination
|
||||||
import dev.jdtech.jellyfin.models.CollectionType
|
import dev.jdtech.jellyfin.models.CollectionType
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidFolder
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
|
@ -65,12 +66,14 @@ fun LibraryScreen(
|
||||||
is FindroidShow -> {
|
is FindroidShow -> {
|
||||||
navigator.navigate(ShowScreenDestination(item.id))
|
navigator.navigate(ShowScreenDestination(item.id))
|
||||||
}
|
}
|
||||||
|
is FindroidFolder -> {
|
||||||
|
navigator.navigate(LibraryScreenDestination(item.id, item.name, libraryType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryScreenLayout(
|
private fun LibraryScreenLayout(
|
||||||
libraryName: String,
|
libraryName: String,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
@ -32,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
@ -86,7 +86,6 @@ fun LoginScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoginScreenLayout(
|
private fun LoginScreenLayout(
|
||||||
uiState: LoginViewModel.UiState,
|
uiState: LoginViewModel.UiState,
|
||||||
|
@ -110,6 +109,14 @@ private fun LoginScreenLayout(
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var disclaimer: String? by remember {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState is LoginViewModel.UiState.Normal) {
|
||||||
|
disclaimer = uiState.disclaimer
|
||||||
|
}
|
||||||
|
|
||||||
val isError = uiState is LoginViewModel.UiState.Error
|
val isError = uiState is LoginViewModel.UiState.Error
|
||||||
val isLoading = uiState is LoginViewModel.UiState.Loading
|
val isLoading = uiState is LoginViewModel.UiState.Loading
|
||||||
|
|
||||||
|
@ -241,6 +248,10 @@ private fun LoginScreenLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
text = disclaimer ?: "",
|
||||||
|
modifier = Modifier.padding(MaterialTheme.spacings.default),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +265,7 @@ private fun LoginScreenLayout(
|
||||||
private fun LoginScreenLayoutPreview() {
|
private fun LoginScreenLayoutPreview() {
|
||||||
FindroidTheme {
|
FindroidTheme {
|
||||||
LoginScreenLayout(
|
LoginScreenLayout(
|
||||||
uiState = LoginViewModel.UiState.Normal,
|
uiState = LoginViewModel.UiState.Normal(),
|
||||||
quickConnectUiState = LoginViewModel.QuickConnectUiState.Normal,
|
quickConnectUiState = LoginViewModel.QuickConnectUiState.Normal,
|
||||||
onLoginClick = { _, _ -> },
|
onLoginClick = { _, _ -> },
|
||||||
onQuickConnectClick = {},
|
onQuickConnectClick = {},
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Tab
|
import androidx.tv.material3.Tab
|
||||||
|
@ -78,7 +77,6 @@ enum class TabDestination(
|
||||||
// LiveTV(CoreR.drawable.ic_tv, CoreR.string.live_tv)
|
// LiveTV(CoreR.drawable.ic_tv, CoreR.string.live_tv)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainScreenLayout(
|
private fun MainScreenLayout(
|
||||||
uiState: MainViewModel.UiState,
|
uiState: MainViewModel.UiState,
|
||||||
|
|
|
@ -39,7 +39,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.toSize
|
import androidx.compose.ui.unit.toSize
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
@ -62,6 +61,7 @@ import dev.jdtech.jellyfin.viewmodels.MovieViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent
|
import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
|
@ -115,7 +115,6 @@ fun MovieScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MovieScreenLayout(
|
private fun MovieScreenLayout(
|
||||||
uiState: MovieViewModel.UiState,
|
uiState: MovieViewModel.UiState,
|
||||||
|
@ -345,6 +344,7 @@ private fun MovieScreenLayoutPreview() {
|
||||||
director = BaseItemPerson(
|
director = BaseItemPerson(
|
||||||
id = UUID.randomUUID(),
|
id = UUID.randomUUID(),
|
||||||
name = "Robert Rodriguez",
|
name = "Robert Rodriguez",
|
||||||
|
type = PersonKind.DIRECTOR,
|
||||||
),
|
),
|
||||||
writers = emptyList(),
|
writers = emptyList(),
|
||||||
videoMetadata = VideoMetadata(
|
videoMetadata = VideoMetadata(
|
||||||
|
|
|
@ -30,7 +30,6 @@ import androidx.media3.common.TrackSelectionOverride
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
@ -204,7 +203,6 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@androidx.annotation.OptIn(UnstableApi::class)
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerControls(
|
fun VideoPlayerControls(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
@ -76,7 +75,6 @@ fun SeasonScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SeasonScreenLayout(
|
private fun SeasonScreenLayout(
|
||||||
seriesName: String,
|
seriesName: String,
|
||||||
|
|
|
@ -35,7 +35,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.OutlinedButton
|
import androidx.tv.material3.OutlinedButton
|
||||||
|
@ -103,7 +102,6 @@ fun ServerSelectScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ServerSelectScreenLayout(
|
private fun ServerSelectScreenLayout(
|
||||||
uiState: ServerSelectViewModel.UiState,
|
uiState: ServerSelectViewModel.UiState,
|
||||||
|
@ -246,7 +244,6 @@ private fun ServerSelectScreenLayoutPreviewNoServers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ServerComponent(
|
private fun ServerComponent(
|
||||||
server: DiscoveredServer,
|
server: DiscoveredServer,
|
||||||
|
|
|
@ -19,7 +19,6 @@ import androidx.tv.foundation.lazy.grid.TvGridCells
|
||||||
import androidx.tv.foundation.lazy.grid.TvGridItemSpan
|
import androidx.tv.foundation.lazy.grid.TvGridItemSpan
|
||||||
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid
|
||||||
import androidx.tv.foundation.lazy.grid.items
|
import androidx.tv.foundation.lazy.grid.items
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
@ -80,7 +79,6 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreenLayout(
|
private fun SettingsScreenLayout(
|
||||||
uiState: SettingsViewModel.UiState,
|
uiState: SettingsViewModel.UiState,
|
||||||
|
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
@ -90,7 +89,6 @@ fun SettingsSubScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSubScreenLayout(
|
private fun SettingsSubScreenLayout(
|
||||||
uiState: SettingsViewModel.UiState,
|
uiState: SettingsViewModel.UiState,
|
||||||
|
|
|
@ -49,7 +49,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.foundation.lazy.list.rememberTvLazyListState
|
import androidx.tv.foundation.lazy.list.rememberTvLazyListState
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
@ -126,7 +125,6 @@ fun ShowScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShowScreenLayout(
|
private fun ShowScreenLayout(
|
||||||
uiState: ShowViewModel.UiState,
|
uiState: ShowViewModel.UiState,
|
||||||
|
|
|
@ -33,7 +33,6 @@ import androidx.tv.foundation.lazy.list.TvLazyRow
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.OutlinedButton
|
import androidx.tv.material3.OutlinedButton
|
||||||
|
@ -99,7 +98,6 @@ fun UserSelectScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserSelectScreenLayout(
|
private fun UserSelectScreenLayout(
|
||||||
uiState: UserSelectViewModel.UiState,
|
uiState: UserSelectViewModel.UiState,
|
||||||
|
@ -204,7 +202,6 @@ private fun UserSelectScreenLayoutPreviewNoUsers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UserComponent(
|
private fun UserComponent(
|
||||||
user: User,
|
user: User,
|
||||||
|
|
|
@ -23,7 +23,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyEpisode
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EpisodeCard(
|
fun EpisodeCard(
|
||||||
episode: FindroidEpisode,
|
episode: FindroidEpisode,
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
@ -33,7 +32,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyMovie
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemCard(
|
fun ItemCard(
|
||||||
item: FindroidItem,
|
item: FindroidItem,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import dev.jdtech.jellyfin.models.FindroidEpisode
|
import dev.jdtech.jellyfin.models.FindroidEpisode
|
||||||
|
@ -17,7 +16,6 @@ enum class Direction {
|
||||||
HORIZONTAL, VERTICAL
|
HORIZONTAL, VERTICAL
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemPoster(
|
fun ItemPoster(
|
||||||
item: FindroidItem,
|
item: FindroidItem,
|
||||||
|
|
|
@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.height
|
import androidx.compose.ui.unit.height
|
||||||
import androidx.compose.ui.unit.width
|
import androidx.compose.ui.unit.width
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.TabRow
|
import androidx.tv.material3.TabRow
|
||||||
|
|
||||||
|
@ -35,7 +34,6 @@ import androidx.tv.material3.TabRow
|
||||||
*
|
*
|
||||||
* This component is adapted from androidx.tv.material3.TabRowDefaults.PillIndicator
|
* This component is adapted from androidx.tv.material3.TabRowDefaults.PillIndicator
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PillBorderIndicator(
|
fun PillBorderIndicator(
|
||||||
currentTabPosition: DpRect,
|
currentTabPosition: DpRect,
|
||||||
|
|
|
@ -17,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
@ -29,7 +28,6 @@ import dev.jdtech.jellyfin.ui.dummy.dummyUser
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileButton(
|
fun ProfileButton(
|
||||||
user: User?,
|
user: User?,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
|
@ -25,7 +24,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProgressBadge(
|
fun ProgressBadge(
|
||||||
item: FindroidItem,
|
item: FindroidItem,
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
@ -31,7 +30,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsCategoryCard(
|
fun SettingsCategoryCard(
|
||||||
preference: PreferenceCategory,
|
preference: PreferenceCategory,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.RadioButton
|
import androidx.tv.material3.RadioButton
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
@ -33,7 +32,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsDetailsCard(
|
fun SettingsDetailsCard(
|
||||||
preference: PreferenceSelect,
|
preference: PreferenceSelect,
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsSelectCard(
|
fun SettingsSelectCard(
|
||||||
preference: PreferenceSelect,
|
preference: PreferenceSelect,
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
@ -32,7 +31,6 @@ import dev.jdtech.jellyfin.models.PreferenceSwitch
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsSwitchCard(
|
fun SettingsSwitchCard(
|
||||||
preference: PreferenceSwitch,
|
preference: PreferenceSwitch,
|
||||||
|
|
|
@ -15,12 +15,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerControlsLayout(
|
fun VideoPlayerControlsLayout(
|
||||||
mediaTitle: @Composable () -> Unit,
|
mediaTitle: @Composable () -> Unit,
|
||||||
|
|
|
@ -7,11 +7,9 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.IconButton
|
import androidx.tv.material3.IconButton
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerMediaButton(
|
fun VideoPlayerMediaButton(
|
||||||
icon: Painter,
|
icon: Painter,
|
||||||
|
|
|
@ -4,12 +4,10 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerMediaTitle(
|
fun VideoPlayerMediaTitle(
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
@ -21,12 +21,10 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.spacings
|
import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerOverlay(
|
fun VideoPlayerOverlay(
|
||||||
isPlaying: Boolean,
|
isPlaying: Boolean,
|
||||||
|
|
|
@ -25,12 +25,11 @@ import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
import dev.jdtech.jellyfin.ui.theme.FindroidTheme
|
||||||
import dev.jdtech.jellyfin.utils.handleDPadKeyEvents
|
import dev.jdtech.jellyfin.utils.handleDPadKeyEvents
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerSeekBar(
|
fun VideoPlayerSeekBar(
|
||||||
progress: Float,
|
progress: Float,
|
||||||
|
|
|
@ -15,7 +15,6 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.IconButton
|
import androidx.tv.material3.IconButton
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
@ -25,7 +24,6 @@ import dev.jdtech.jellyfin.ui.theme.spacings
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerSeeker(
|
fun VideoPlayerSeeker(
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
|
|
|
@ -24,7 +24,6 @@ import androidx.tv.foundation.lazy.list.items
|
||||||
import androidx.tv.material3.Border
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceScale
|
import androidx.tv.material3.ClickableSurfaceScale
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.RadioButton
|
import androidx.tv.material3.RadioButton
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
@ -45,7 +44,6 @@ data class VideoPlayerTrackSelectorDialogResult(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Destination(style = BaseDialogStyle::class)
|
@Destination(style = BaseDialogStyle::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoPlayerTrackSelectorDialog(
|
fun VideoPlayerTrackSelectorDialog(
|
||||||
|
|
|
@ -55,6 +55,8 @@ val dummyEpisode = FindroidEpisode(
|
||||||
seasonId = UUID.randomUUID(),
|
seasonId = UUID.randomUUID(),
|
||||||
communityRating = 9.2f,
|
communityRating = 9.2f,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
|
trickplayInfo = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyEpisodes = listOf(
|
val dummyEpisodes = listOf(
|
||||||
|
|
|
@ -55,6 +55,8 @@ val dummyMovie = FindroidMovie(
|
||||||
endDate = null,
|
endDate = null,
|
||||||
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
|
trickplayInfo = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyMovies = listOf(
|
val dummyMovies = listOf(
|
||||||
|
|
|
@ -2,7 +2,6 @@ package dev.jdtech.jellyfin.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.darkColorScheme as darkColorSchemeTv
|
import androidx.tv.material3.darkColorScheme as darkColorSchemeTv
|
||||||
|
|
||||||
val PrimaryDark = Color(0xffa1c9ff)
|
val PrimaryDark = Color(0xffa1c9ff)
|
||||||
|
@ -23,7 +22,6 @@ val ColorScheme = darkColorScheme(
|
||||||
background = Neutral1000,
|
background = Neutral1000,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
val ColorSchemeTv = darkColorSchemeTv(
|
val ColorSchemeTv = darkColorSchemeTv(
|
||||||
primary = ColorScheme.primary,
|
primary = ColorScheme.primary,
|
||||||
onPrimary = ColorScheme.onPrimary,
|
onPrimary = ColorScheme.onPrimary,
|
||||||
|
|
|
@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.ui.theme
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Shapes
|
import androidx.compose.material3.Shapes
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Shapes as ShapesTv
|
import androidx.tv.material3.Shapes as ShapesTv
|
||||||
|
|
||||||
val shapes = Shapes(
|
val shapes = Shapes(
|
||||||
|
@ -11,7 +10,6 @@ val shapes = Shapes(
|
||||||
small = RoundedCornerShape(10.dp),
|
small = RoundedCornerShape(10.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
val shapesTv = ShapesTv(
|
val shapesTv = ShapesTv(
|
||||||
extraSmall = shapes.extraSmall,
|
extraSmall = shapes.extraSmall,
|
||||||
small = shapes.small,
|
small = shapes.small,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -17,9 +16,7 @@ data class Spacings(
|
||||||
val extraLarge: Dp = 64.dp,
|
val extraLarge: Dp = 64.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
val MaterialTheme.spacings
|
val MaterialTheme.spacings
|
||||||
get() = Spacings()
|
get() = Spacings()
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
val LocalSpacings = compositionLocalOf { MaterialTheme.spacings }
|
val LocalSpacings = compositionLocalOf { MaterialTheme.spacings }
|
||||||
|
|
|
@ -10,12 +10,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.NonInteractiveSurfaceDefaults
|
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
|
import androidx.tv.material3.SurfaceDefaults
|
||||||
import androidx.tv.material3.MaterialTheme as MaterialThemeTv
|
import androidx.tv.material3.MaterialTheme as MaterialThemeTv
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FindroidTheme(
|
fun FindroidTheme(
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
|
@ -34,7 +32,7 @@ fun FindroidTheme(
|
||||||
shapes = shapesTv,
|
shapes = shapesTv,
|
||||||
content = {
|
content = {
|
||||||
Surface(
|
Surface(
|
||||||
colors = NonInteractiveSurfaceDefaults.colors(
|
colors = SurfaceDefaults.colors(
|
||||||
containerColor = androidx.tv.material3.MaterialTheme.colorScheme.background,
|
containerColor = androidx.tv.material3.MaterialTheme.colorScheme.background,
|
||||||
),
|
),
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
|
||||||
import androidx.tv.material3.Typography as TypographyTv
|
import androidx.tv.material3.Typography as TypographyTv
|
||||||
|
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
|
@ -34,7 +33,6 @@ val Typography = Typography(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
||||||
val TypographyTv = TypographyTv(
|
val TypographyTv = TypographyTv(
|
||||||
displayMedium = Typography.displayMedium,
|
displayMedium = Typography.displayMedium,
|
||||||
headlineMedium = Typography.headlineMedium,
|
headlineMedium = Typography.headlineMedium,
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import com.android.build.api.dsl.CommonExtension
|
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.android.library) apply false
|
alias(libs.plugins.android.library) apply false
|
||||||
|
@ -19,17 +16,6 @@ allprojects {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
val configureAndroid = { _: AppliedPlugin ->
|
|
||||||
extensions.configure<CommonExtension<*, *, *, *, *>>("android") {
|
|
||||||
lint {
|
|
||||||
informational += "MissingTranslation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginManager.withPlugin("com.android.library", configureAndroid)
|
|
||||||
pluginManager.withPlugin("com.android.application", configureAndroid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.create<Delete>("clean") {
|
tasks.create<Delete>("clean") {
|
||||||
|
|
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
|
import org.gradle.api.JavaVersion
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val appCode = 22
|
const val appCode = 25
|
||||||
const val appName = "0.13.1"
|
const val appName = "0.14.2"
|
||||||
|
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
const val buildTools = "34.0.0"
|
const val buildTools = "34.0.0"
|
||||||
|
@ -11,6 +11,5 @@ object Versions {
|
||||||
|
|
||||||
val java = JavaVersion.VERSION_17
|
val java = JavaVersion.VERSION_17
|
||||||
|
|
||||||
const val composeCompiler = "1.5.7"
|
|
||||||
const val ktlint = "0.50.0"
|
const val ktlint = "0.50.0"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose.compiler)
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.androidx.navigation.safeargs)
|
alias(libs.plugins.androidx.navigation.safeargs)
|
||||||
|
@ -29,7 +30,6 @@ android {
|
||||||
flavorDimensions += "variant"
|
flavorDimensions += "variant"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
register("libre")
|
register("libre")
|
||||||
register("huawei")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -40,10 +40,6 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = Versions.composeCompiler
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ktlint {
|
ktlint {
|
||||||
|
@ -53,18 +49,18 @@ ktlint {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":data"))
|
val composeBom = platform(libs.androidx.compose.bom)
|
||||||
implementation(project(":preferences"))
|
|
||||||
implementation(project(":player:core"))
|
implementation(projects.data)
|
||||||
implementation(libs.androidx.activity)
|
implementation(projects.preferences)
|
||||||
|
implementation(projects.player.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(composeBom)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.hilt.work)
|
implementation(libs.androidx.hilt.work)
|
||||||
ksp(libs.androidx.hilt.compiler)
|
ksp(libs.androidx.hilt.compiler)
|
||||||
implementation(libs.androidx.lifecycle.runtime)
|
|
||||||
implementation(libs.androidx.lifecycle.viewmodel)
|
implementation(libs.androidx.lifecycle.viewmodel)
|
||||||
implementation(libs.androidx.navigation.fragment)
|
|
||||||
implementation(libs.androidx.paging)
|
implementation(libs.androidx.paging)
|
||||||
implementation(libs.androidx.preference)
|
implementation(libs.androidx.preference)
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
|
|
|
@ -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
|
val user = serverWithAddressAndUser.user
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = serverAddress.address
|
api.update(
|
||||||
api.accessToken = user?.accessToken
|
baseUrl = serverAddress.address,
|
||||||
|
accessToken = user?.accessToken,
|
||||||
|
)
|
||||||
userId = user?.id
|
userId = user?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.InputType
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
@ -15,13 +16,14 @@ class AddServerAddressDialog(
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val editText = EditText(this.context)
|
val editText = EditText(this.context)
|
||||||
editText.hint = "http://<server_ip>:8096"
|
editText.hint = "http://<server_ip>:8096"
|
||||||
|
editText.inputType = InputType.TYPE_TEXT_VARIATION_URI
|
||||||
return activity?.let { activity ->
|
return activity?.let { activity ->
|
||||||
val builder = MaterialAlertDialogBuilder(activity)
|
val builder = MaterialAlertDialogBuilder(activity)
|
||||||
builder
|
builder
|
||||||
.setTitle(getString(R.string.add_server_address))
|
.setTitle(getString(R.string.add_server_address))
|
||||||
.setView(editText)
|
.setView(editText)
|
||||||
.setPositiveButton(getString(R.string.add)) { _, _ ->
|
.setPositiveButton(getString(R.string.add)) { _, _ ->
|
||||||
viewModel.addAddress(editText.text.toString())
|
viewModel.addAddress(requireContext(), editText.text.toString())
|
||||||
}
|
}
|
||||||
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
|
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ class SortDialogFragment(
|
||||||
when (sortType) {
|
when (sortType) {
|
||||||
"sortBy" -> {
|
"sortBy" -> {
|
||||||
val sortByOptions = resources.getStringArray(R.array.sort_by_options)
|
val sortByOptions = resources.getStringArray(R.array.sort_by_options)
|
||||||
val sortByValues = SortBy.values()
|
val sortByValues = SortBy.entries
|
||||||
builder
|
builder
|
||||||
.setTitle(getString(R.string.sort_by))
|
.setTitle(getString(R.string.sort_by))
|
||||||
.setSingleChoiceItems(
|
.setSingleChoiceItems(
|
||||||
|
@ -64,7 +64,7 @@ class SortDialogFragment(
|
||||||
}
|
}
|
||||||
"sortOrder" -> {
|
"sortOrder" -> {
|
||||||
val sortByOptions = resources.getStringArray(R.array.sort_order_options)
|
val sortByOptions = resources.getStringArray(R.array.sort_order_options)
|
||||||
val sortOrderValues = SortOrder.values()
|
val sortOrderValues = SortOrder.entries
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.setTitle(getString(R.string.sort_order))
|
.setTitle(getString(R.string.sort_order))
|
||||||
|
|
|
@ -18,7 +18,7 @@ fun BaseItemDto.toView(): View {
|
||||||
return View(
|
return View(
|
||||||
id = id,
|
id = id,
|
||||||
name = name ?: "",
|
name = name ?: "",
|
||||||
type = CollectionType.fromString(collectionType),
|
type = CollectionType.fromString(collectionType?.serialName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@ import dev.jdtech.jellyfin.models.FindroidEpisode
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidSource
|
import dev.jdtech.jellyfin.models.FindroidSource
|
||||||
import dev.jdtech.jellyfin.models.TrickPlayManifest
|
import dev.jdtech.jellyfin.models.FindroidSources
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidTrickplayInfo
|
||||||
import dev.jdtech.jellyfin.models.UiText
|
import dev.jdtech.jellyfin.models.UiText
|
||||||
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
|
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
|
||||||
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
|
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
|
||||||
|
@ -21,13 +22,14 @@ import dev.jdtech.jellyfin.models.toFindroidMovieDto
|
||||||
import dev.jdtech.jellyfin.models.toFindroidSeasonDto
|
import dev.jdtech.jellyfin.models.toFindroidSeasonDto
|
||||||
import dev.jdtech.jellyfin.models.toFindroidShowDto
|
import dev.jdtech.jellyfin.models.toFindroidShowDto
|
||||||
import dev.jdtech.jellyfin.models.toFindroidSourceDto
|
import dev.jdtech.jellyfin.models.toFindroidSourceDto
|
||||||
|
import dev.jdtech.jellyfin.models.toFindroidTrickplayInfoDto
|
||||||
import dev.jdtech.jellyfin.models.toFindroidUserDataDto
|
import dev.jdtech.jellyfin.models.toFindroidUserDataDto
|
||||||
import dev.jdtech.jellyfin.models.toIntroDto
|
import dev.jdtech.jellyfin.models.toIntroDto
|
||||||
import dev.jdtech.jellyfin.models.toTrickPlayManifestDto
|
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
|
import kotlin.math.ceil
|
||||||
import dev.jdtech.jellyfin.core.R as CoreR
|
import dev.jdtech.jellyfin.core.R as CoreR
|
||||||
|
|
||||||
class DownloaderImpl(
|
class DownloaderImpl(
|
||||||
|
@ -46,12 +48,8 @@ class DownloaderImpl(
|
||||||
try {
|
try {
|
||||||
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
||||||
val intro = jellyfinRepository.getIntroTimestamps(item.id)
|
val intro = jellyfinRepository.getIntroTimestamps(item.id)
|
||||||
val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id)
|
val trickplayInfo = if (item is FindroidSources) {
|
||||||
val trickPlayData = if (trickPlayManifest != null) {
|
item.trickplayInfo?.get(sourceId)
|
||||||
jellyfinRepository.getTrickPlayData(
|
|
||||||
item.id,
|
|
||||||
trickPlayManifest.widthResolutions.max(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -78,12 +76,12 @@ class DownloaderImpl(
|
||||||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
downloadExternalMediaStreams(item, source, storageIndex)
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
|
if (trickplayInfo != null) {
|
||||||
|
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
||||||
|
}
|
||||||
if (intro != null) {
|
if (intro != null) {
|
||||||
database.insertIntro(intro.toIntroDto(item.id))
|
database.insertIntro(intro.toIntroDto(item.id))
|
||||||
}
|
}
|
||||||
if (trickPlayManifest != null && trickPlayData != null) {
|
|
||||||
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
|
|
||||||
}
|
|
||||||
val request = DownloadManager.Request(source.path.toUri())
|
val request = DownloadManager.Request(source.path.toUri())
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
|
@ -107,12 +105,12 @@ class DownloaderImpl(
|
||||||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
downloadExternalMediaStreams(item, source, storageIndex)
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
|
if (trickplayInfo != null) {
|
||||||
|
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
||||||
|
}
|
||||||
if (intro != null) {
|
if (intro != null) {
|
||||||
database.insertIntro(intro.toIntroDto(item.id))
|
database.insertIntro(intro.toIntroDto(item.id))
|
||||||
}
|
}
|
||||||
if (trickPlayManifest != null && trickPlayData != null) {
|
|
||||||
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
|
|
||||||
}
|
|
||||||
val request = DownloadManager.Request(source.path.toUri())
|
val request = DownloadManager.Request(source.path.toUri())
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
|
@ -175,8 +173,7 @@ class DownloaderImpl(
|
||||||
|
|
||||||
database.deleteIntro(item.id)
|
database.deleteIntro(item.id)
|
||||||
|
|
||||||
database.deleteTrickPlayManifest(item.id)
|
File(context.filesDir, "trickplay/${item.id}").deleteRecursively()
|
||||||
File(context.filesDir, "trickplay/${item.id}.bif").delete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getProgress(downloadId: Long?): Pair<Int, Int> {
|
override suspend fun getProgress(downloadId: Long?): Pair<Int, Int> {
|
||||||
|
@ -233,14 +230,37 @@ class DownloaderImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadTrickPlay(
|
private suspend fun downloadTrickplayData(
|
||||||
item: FindroidItem,
|
itemId: UUID,
|
||||||
trickPlayManifest: TrickPlayManifest,
|
sourceId: String,
|
||||||
byteArray: ByteArray,
|
trickplayInfo: FindroidTrickplayInfo,
|
||||||
) {
|
) {
|
||||||
database.insertTrickPlayManifest(trickPlayManifest.toTrickPlayManifestDto(item.id))
|
val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt()
|
||||||
File(context.filesDir, "trickplay").mkdirs()
|
val byteArrays = mutableListOf<ByteArray>()
|
||||||
val file = File(context.filesDir, "trickplay/${item.id}.bif")
|
for (i in 0..maxIndex) {
|
||||||
file.writeBytes(byteArray)
|
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
|
appPreferences.currentServer = server.id
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = recommendedServerInfo.address
|
api.update(
|
||||||
api.accessToken = null
|
baseUrl = recommendedServerInfo.address,
|
||||||
|
accessToken = null,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.emit(UiState.Normal)
|
_uiState.emit(UiState.Normal)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.models.FavoriteSection
|
||||||
import dev.jdtech.jellyfin.models.FindroidEpisode
|
import dev.jdtech.jellyfin.models.FindroidEpisode
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
|
import dev.jdtech.jellyfin.models.SortBy
|
||||||
import dev.jdtech.jellyfin.models.UiText
|
import dev.jdtech.jellyfin.models.UiText
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -39,7 +40,10 @@ constructor(
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItems(parentId = parentId)
|
val items = jellyfinRepository.getItems(
|
||||||
|
parentId = parentId,
|
||||||
|
sortBy = SortBy.RELEASE_DATE,
|
||||||
|
)
|
||||||
|
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
_uiState.emit(UiState.Normal(emptyList()))
|
_uiState.emit(UiState.Normal(emptyList()))
|
||||||
|
|
|
@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.models.HomeSection
|
||||||
import dev.jdtech.jellyfin.models.UiText
|
import dev.jdtech.jellyfin.models.UiText
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.toView
|
import dev.jdtech.jellyfin.utils.toView
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -41,13 +42,12 @@ class HomeViewModel @Inject internal constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
repository.postCapabilities()
|
repository.postCapabilities()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) { }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData() {
|
fun loadData() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = mutableListOf<HomeItem>()
|
val items = mutableListOf<HomeItem>()
|
||||||
|
@ -93,7 +93,7 @@ class HomeViewModel @Inject internal constructor(
|
||||||
|
|
||||||
private suspend fun loadViews() = repository
|
private suspend fun loadViews() = repository
|
||||||
.getUserViews()
|
.getUserViews()
|
||||||
.filter { view -> CollectionType.supported.any { it.type == view.collectionType } }
|
.filter { view -> CollectionType.fromString(view.collectionType?.serialName) in CollectionType.supported }
|
||||||
.map { view -> view to repository.getLatestMedia(view.id) }
|
.map { view -> view to repository.getLatestMedia(view.id) }
|
||||||
.filter { (_, latest) -> latest.isNotEmpty() }
|
.filter { (_, latest) -> latest.isNotEmpty() }
|
||||||
.map { (view, latest) -> view.toView().apply { items = latest } }
|
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||||
|
|
|
@ -48,16 +48,20 @@ constructor(
|
||||||
CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
|
CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
|
||||||
CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
|
CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
|
||||||
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
|
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
|
||||||
|
CollectionType.Mixed -> listOf(BaseItemKind.FOLDER, BaseItemKind.MOVIE, BaseItemKind.SERIES)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val recursive = itemType == null || !itemType.contains(BaseItemKind.FOLDER)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItemsPaging(
|
val items = jellyfinRepository.getItemsPaging(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
includeTypes = itemType,
|
includeTypes = itemType,
|
||||||
recursive = true,
|
recursive = recursive,
|
||||||
sortBy = sortBy,
|
sortBy = if (libraryType == CollectionType.TvShows && sortBy == SortBy.DATE_PLAYED) SortBy.SERIES_DATE_PLAYED else sortBy, // Jellyfin uses a different enum for sorting series by data played
|
||||||
sortOrder = sortOrder,
|
sortOrder = sortOrder,
|
||||||
).cachedIn(viewModelScope)
|
).cachedIn(viewModelScope)
|
||||||
_uiState.emit(UiState.Normal(items))
|
_uiState.emit(UiState.Normal(items))
|
||||||
|
|
|
@ -32,7 +32,7 @@ constructor(
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val database: ServerDatabaseDao,
|
private val database: ServerDatabaseDao,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow<UiState>(UiState.Normal)
|
private val _uiState = MutableStateFlow<UiState>(UiState.Normal())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
|
private val _usersState = MutableStateFlow<UsersState>(UsersState.Loading)
|
||||||
val usersState = _usersState.asStateFlow()
|
val usersState = _usersState.asStateFlow()
|
||||||
|
@ -44,8 +44,10 @@ constructor(
|
||||||
|
|
||||||
private var quickConnectJob: Job? = null
|
private var quickConnectJob: Job? = null
|
||||||
|
|
||||||
|
private var loginDisclaimer: String? = null
|
||||||
|
|
||||||
sealed class UiState {
|
sealed class UiState {
|
||||||
data object Normal : UiState()
|
data class Normal(val disclaimer: String? = null) : UiState()
|
||||||
data object Loading : UiState()
|
data object Loading : UiState()
|
||||||
data class Error(val message: UiText) : UiState()
|
data class Error(val message: UiText) : UiState()
|
||||||
}
|
}
|
||||||
|
@ -62,10 +64,18 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
loadDisclaimer()
|
||||||
loadPublicUsers()
|
loadPublicUsers()
|
||||||
loadQuickConnectAvailable()
|
loadQuickConnectAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadDisclaimer() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loginDisclaimer = jellyfinApi.brandingApi.getBrandingOptions().content.loginDisclaimer
|
||||||
|
_uiState.emit(UiState.Normal(loginDisclaimer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadPublicUsers() {
|
private fun loadPublicUsers() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_usersState.emit(UsersState.Loading)
|
_usersState.emit(UsersState.Loading)
|
||||||
|
@ -93,7 +103,7 @@ constructor(
|
||||||
private fun loadQuickConnectAvailable() {
|
private fun loadQuickConnectAvailable() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val isEnabled by jellyfinApi.quickConnectApi.getEnabled()
|
val isEnabled by jellyfinApi.quickConnectApi.getQuickConnectEnabled()
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
_quickConnectUiState.emit(QuickConnectUiState.Normal)
|
_quickConnectUiState.emit(QuickConnectUiState.Normal)
|
||||||
}
|
}
|
||||||
|
@ -121,7 +131,7 @@ constructor(
|
||||||
|
|
||||||
saveAuthenticationResult(authenticationResult)
|
saveAuthenticationResult(authenticationResult)
|
||||||
|
|
||||||
_uiState.emit(UiState.Normal)
|
_uiState.emit(UiState.Normal(loginDisclaimer))
|
||||||
eventsChannel.send(LoginEvent.NavigateToHome)
|
eventsChannel.send(LoginEvent.NavigateToHome)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val message =
|
val message =
|
||||||
|
@ -144,12 +154,12 @@ constructor(
|
||||||
}
|
}
|
||||||
quickConnectJob = viewModelScope.launch {
|
quickConnectJob = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
var quickConnectState = jellyfinApi.quickConnectApi.initiate().content
|
var quickConnectState = jellyfinApi.quickConnectApi.initiateQuickConnect().content
|
||||||
_quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
|
_quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
|
||||||
|
|
||||||
while (!quickConnectState.authenticated) {
|
while (!quickConnectState.authenticated) {
|
||||||
quickConnectState = jellyfinApi.quickConnectApi.connect(quickConnectState.secret).content
|
|
||||||
delay(5000L)
|
delay(5000L)
|
||||||
|
quickConnectState = jellyfinApi.quickConnectApi.getQuickConnectState(quickConnectState.secret).content
|
||||||
}
|
}
|
||||||
val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
|
val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
|
||||||
secret = quickConnectState.secret,
|
secret = quickConnectState.secret,
|
||||||
|
@ -178,7 +188,7 @@ constructor(
|
||||||
insertUser(appPreferences.currentServer!!, user)
|
insertUser(appPreferences.currentServer!!, user)
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.accessToken = authenticationResult.accessToken
|
api.update(accessToken = authenticationResult.accessToken)
|
||||||
userId = authenticationResult.user?.id
|
userId = authenticationResult.user?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.CollectionType
|
|
||||||
import dev.jdtech.jellyfin.models.FindroidCollection
|
import dev.jdtech.jellyfin.models.FindroidCollection
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -35,9 +34,7 @@ constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getLibraries()
|
val collections = jellyfinRepository.getLibraries()
|
||||||
val collections =
|
|
||||||
items.filter { collection -> collection.type in CollectionType.supported }
|
|
||||||
_uiState.emit(UiState.Normal(collections))
|
_uiState.emit(UiState.Normal(collections))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_uiState.emit(
|
_uiState.emit(
|
||||||
|
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
|
import org.jellyfin.sdk.model.api.VideoRangeType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -119,7 +121,7 @@ constructor(
|
||||||
private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> {
|
private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> {
|
||||||
val actors: List<BaseItemPerson>
|
val actors: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
actors = item.people.filter { it.type == "Actor" }
|
actors = item.people.filter { it.type == PersonKind.ACTOR }
|
||||||
}
|
}
|
||||||
return actors
|
return actors
|
||||||
}
|
}
|
||||||
|
@ -127,7 +129,7 @@ constructor(
|
||||||
private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? {
|
private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? {
|
||||||
val director: BaseItemPerson?
|
val director: BaseItemPerson?
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
director = item.people.firstOrNull { it.type == "Director" }
|
director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
|
||||||
}
|
}
|
||||||
return director
|
return director
|
||||||
}
|
}
|
||||||
|
@ -135,7 +137,7 @@ constructor(
|
||||||
private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> {
|
private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> {
|
||||||
val writers: List<BaseItemPerson>
|
val writers: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
writers = item.people.filter { it.type == "Writer" }
|
writers = item.people.filter { it.type == PersonKind.WRITER }
|
||||||
}
|
}
|
||||||
return writers
|
return writers
|
||||||
}
|
}
|
||||||
|
@ -213,9 +215,9 @@ constructor(
|
||||||
DisplayProfile.DOLBY_VISION
|
DisplayProfile.DOLBY_VISION
|
||||||
} else {
|
} else {
|
||||||
when (videoRangeType) {
|
when (videoRangeType) {
|
||||||
DisplayProfile.HDR.raw -> DisplayProfile.HDR
|
VideoRangeType.HDR10 -> DisplayProfile.HDR10
|
||||||
DisplayProfile.HDR10.raw -> DisplayProfile.HDR10
|
VideoRangeType.HDR10_PLUS -> DisplayProfile.HDR10_PLUS
|
||||||
DisplayProfile.HLG.raw -> DisplayProfile.HLG
|
VideoRangeType.HLG -> DisplayProfile.HLG
|
||||||
else -> DisplayProfile.SDR
|
else -> DisplayProfile.SDR
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -73,17 +74,29 @@ constructor(
|
||||||
server.currentServerAddressId = address.id
|
server.currentServerAddressId = address.id
|
||||||
database.update(server)
|
database.update(server)
|
||||||
|
|
||||||
jellyfinApi.api.baseUrl = address.address
|
jellyfinApi.api.update(
|
||||||
|
baseUrl = address.address,
|
||||||
|
)
|
||||||
|
|
||||||
eventsChannel.send(ServerAddressesEvent.NavigateToHome)
|
eventsChannel.send(ServerAddressesEvent.NavigateToHome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAddress(address: String) {
|
fun addAddress(context: Context, address: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val serverAddress = ServerAddress(UUID.randomUUID(), currentServerId, address)
|
try {
|
||||||
database.insertServerAddress(serverAddress)
|
val jellyfinApi = JellyfinApi(context)
|
||||||
loadAddresses(currentServerId)
|
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 server has no selected user, navigate to login fragment
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = serverAddress.address
|
api.update(
|
||||||
api.accessToken = null
|
baseUrl = serverAddress.address,
|
||||||
|
accessToken = null,
|
||||||
|
)
|
||||||
userId = null
|
userId = null
|
||||||
}
|
}
|
||||||
appPreferences.currentServer = server.id
|
appPreferences.currentServer = server.id
|
||||||
|
@ -111,8 +113,10 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = serverAddress.address
|
api.update(
|
||||||
api.accessToken = user.accessToken
|
baseUrl = serverAddress.address,
|
||||||
|
accessToken = user.accessToken,
|
||||||
|
)
|
||||||
userId = user.id
|
userId = user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ constructor(
|
||||||
nameStringResource = R.string.pref_player_mpv_vo,
|
nameStringResource = R.string.pref_player_mpv_vo,
|
||||||
dependencies = listOf(Constants.PREF_PLAYER_MPV),
|
dependencies = listOf(Constants.PREF_PLAYER_MPV),
|
||||||
backendName = Constants.PREF_PLAYER_MPV_VO,
|
backendName = Constants.PREF_PLAYER_MPV_VO,
|
||||||
backendDefaultValue = "gpu",
|
backendDefaultValue = "gpu-next",
|
||||||
options = R.array.mpv_vos,
|
options = R.array.mpv_vos,
|
||||||
optionValues = R.array.mpv_vos,
|
optionValues = R.array.mpv_vos,
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
|
import org.jellyfin.sdk.model.api.PersonKind
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -100,7 +101,7 @@ constructor(
|
||||||
private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> {
|
private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> {
|
||||||
val actors: List<BaseItemPerson>
|
val actors: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
actors = item.people.filter { it.type == "Actor" }
|
actors = item.people.filter { it.type == PersonKind.ACTOR }
|
||||||
}
|
}
|
||||||
return actors
|
return actors
|
||||||
}
|
}
|
||||||
|
@ -108,7 +109,7 @@ constructor(
|
||||||
private suspend fun getDirector(item: FindroidShow): BaseItemPerson? {
|
private suspend fun getDirector(item: FindroidShow): BaseItemPerson? {
|
||||||
val director: BaseItemPerson?
|
val director: BaseItemPerson?
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
director = item.people.firstOrNull { it.type == "Director" }
|
director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
|
||||||
}
|
}
|
||||||
return director
|
return director
|
||||||
}
|
}
|
||||||
|
@ -116,7 +117,7 @@ constructor(
|
||||||
private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> {
|
private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> {
|
||||||
val writers: List<BaseItemPerson>
|
val writers: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
writers = item.people.filter { it.type == "Writer" }
|
writers = item.people.filter { it.type == PersonKind.WRITER }
|
||||||
}
|
}
|
||||||
return writers
|
return writers
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import javax.inject.Inject
|
||||||
class UserSelectViewModel
|
class UserSelectViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val appPreferences: AppPreferences,
|
appPreferences: AppPreferences,
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val database: ServerDatabaseDao,
|
private val database: ServerDatabaseDao,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
@ -71,7 +71,9 @@ constructor(
|
||||||
database.update(server)
|
database.update(server)
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.accessToken = user.accessToken
|
api.update(
|
||||||
|
accessToken = user.accessToken,
|
||||||
|
)
|
||||||
userId = user.id
|
userId = user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,9 @@ constructor(
|
||||||
database.update(server)
|
database.update(server)
|
||||||
|
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.accessToken = user.accessToken
|
api.update(
|
||||||
|
accessToken = user.accessToken,
|
||||||
|
)
|
||||||
userId = user.id
|
userId = user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,10 @@ class SyncWorker @AssistedInject constructor(
|
||||||
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue
|
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue
|
||||||
for (user in serverWithAddressesAndUsers.users) {
|
for (user in serverWithAddressesAndUsers.users) {
|
||||||
jellyfinApi.apply {
|
jellyfinApi.apply {
|
||||||
api.baseUrl = serverAddress.address
|
api.update(
|
||||||
api.accessToken = user.accessToken
|
baseUrl = serverAddress.address,
|
||||||
|
accessToken = user.accessToken,
|
||||||
|
)
|
||||||
userId = user.id
|
userId = user.id
|
||||||
}
|
}
|
||||||
val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) }
|
val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) }
|
||||||
|
@ -66,17 +68,16 @@ class SyncWorker @AssistedInject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
when (userData.played) {
|
when (userData.played) {
|
||||||
true -> jellyfinApi.playStateApi.markPlayedItem(user.id, item.id)
|
true -> jellyfinApi.playStateApi.markPlayedItem(item.id, user.id)
|
||||||
false -> jellyfinApi.playStateApi.markUnplayedItem(user.id, item.id)
|
false -> jellyfinApi.playStateApi.markUnplayedItem(item.id, user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (userData.favorite) {
|
when (userData.favorite) {
|
||||||
true -> jellyfinApi.userLibraryApi.markFavoriteItem(user.id, item.id)
|
true -> jellyfinApi.userLibraryApi.markFavoriteItem(item.id, user.id)
|
||||||
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(user.id, item.id)
|
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(item.id, user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
jellyfinApi.playStateApi.onPlaybackStopped(
|
jellyfinApi.playStateApi.onPlaybackStopped(
|
||||||
userId = user.id,
|
|
||||||
itemId = item.id,
|
itemId = item.id,
|
||||||
positionTicks = userData.playbackPositionTicks,
|
positionTicks = userData.playbackPositionTicks,
|
||||||
)
|
)
|
||||||
|
|
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="pref_player_intro_skipper_summary">Requiere que el complemento Intro Skipper de ConfusedPolarBear esté instalado en el servidor</string>
|
||||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||||
<string name="episode_name_extended_with_end">T%1$d:E%2$d-%3$d - %4$s</string>
|
<string name="episode_name_extended_with_end">T%1$d:E%2$d-%3$d - %4$s</string>
|
||||||
<string name="pref_player_trick_play_summary">Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor</string>
|
|
||||||
<string name="extra_info_summary">Mostrar información detallada de audio, video y subtítulos</string>
|
<string name="extra_info_summary">Mostrar información detallada de audio, video y subtítulos</string>
|
||||||
<string name="offline_mode">Modo desconectado</string>
|
<string name="offline_mode">Modo desconectado</string>
|
||||||
<string name="offline_mode_icon">Icono de modo desconectado</string>
|
<string name="offline_mode_icon">Icono de modo desconectado</string>
|
||||||
|
@ -161,7 +160,6 @@
|
||||||
<string name="player_gestures_seek">Gesto de búsqueda</string>
|
<string name="player_gestures_seek">Gesto de búsqueda</string>
|
||||||
<string name="player_gestures_seek_summary">Deslizar horizontalmente para buscar adelante o atrás</string>
|
<string name="player_gestures_seek_summary">Deslizar horizontalmente para buscar adelante o atrás</string>
|
||||||
<string name="downloaded_indicator">Indicador de descargado</string>
|
<string name="downloaded_indicator">Indicador de descargado</string>
|
||||||
<string name="pref_player_trick_play">Miniaturas (Trick Play)</string>
|
|
||||||
<string name="storage_name">%1$s (%2$d MB libres)</string>
|
<string name="storage_name">%1$s (%2$d MB libres)</string>
|
||||||
<string name="preparing_download">Preparando descarga</string>
|
<string name="preparing_download">Preparando descarga</string>
|
||||||
<string name="cancel_download">Cancelar descarga</string>
|
<string name="cancel_download">Cancelar descarga</string>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="add_server_error_version">Неподдържана версия на сървъра: %1$s. Моля ъпдейтнете вашия сървър</string>
|
<string name="add_server_error_version">Неподдържана версия на сървъра: %1$s. Моля ъпдейтнете вашия сървър</string>
|
||||||
<string name="add_server_error_no_id">Сървърът няма id, изглежда, че има нещо не е наред със сървъра</string>
|
<string name="add_server_error_no_id">Сървърът няма id, изглежда, че нещо не е наред със сървъра</string>
|
||||||
<string name="remove_server">Премахнете сървъра</string>
|
<string name="remove_server">Премахване на сървъра</string>
|
||||||
<string name="sort_by">Сортирайте по</string>
|
<string name="sort_by">Сортирайте по</string>
|
||||||
<string name="download_roaming">Свалете при роуминг</string>
|
<string name="download_roaming">Свалете при роуминг</string>
|
||||||
<string name="mpv_player_summary">Използвайте експерименталният mpv плейър за да пускане на видеа. mpv поддържа повече кодекси за видео, аудио и субтитри.</string>
|
<string name="mpv_player_summary">Използвайте експерименталният mpv плейър за да пускане на видеа. mpv поддържа повече кодекси за видео, аудио и субтитри.</string>
|
||||||
|
@ -13,11 +13,11 @@
|
||||||
<string name="login_error_wrong_username_password">Грешно потребителско име или парола</string>
|
<string name="login_error_wrong_username_password">Грешно потребителско име или парола</string>
|
||||||
<string name="add_server_error_not_jellyfin">Не е Jellyfin сървър: %1$s</string>
|
<string name="add_server_error_not_jellyfin">Не е Jellyfin сървър: %1$s</string>
|
||||||
<string name="add_server_error_slow">Сървърът реагира твърде бавно: %1$s</string>
|
<string name="add_server_error_slow">Сървърът реагира твърде бавно: %1$s</string>
|
||||||
<string name="remove">Премахнете</string>
|
<string name="remove">Премахване</string>
|
||||||
<string name="cancel">Отменете</string>
|
<string name="cancel">Отменяне</string>
|
||||||
<string name="title_home">Начало</string>
|
<string name="title_home">Начало</string>
|
||||||
<string name="button_connect">Свържете се</string>
|
<string name="button_connect">Свързване</string>
|
||||||
<string name="button_login">Влезнете</string>
|
<string name="button_login">Вход</string>
|
||||||
<string name="remove_server_dialog_text">Сигурни ли сте, че искате да премахнете сървъра %1$s</string>
|
<string name="remove_server_dialog_text">Сигурни ли сте, че искате да премахнете сървъра %1$s</string>
|
||||||
<string name="title_media">Моята медия</string>
|
<string name="title_media">Моята медия</string>
|
||||||
<string name="edit_text_server_address_hint">Адрес на сървъра</string>
|
<string name="edit_text_server_address_hint">Адрес на сървъра</string>
|
||||||
|
@ -27,10 +27,10 @@
|
||||||
<string name="title_settings">Настройки</string>
|
<string name="title_settings">Настройки</string>
|
||||||
<string name="title_download">Свалени</string>
|
<string name="title_download">Свалени</string>
|
||||||
<string name="view_all">Вижте всички</string>
|
<string name="view_all">Вижте всички</string>
|
||||||
<string name="error_loading_data">Проблем с зареждането на датата</string>
|
<string name="error_loading_data">Проблем с зареждането на информацията</string>
|
||||||
<string name="retry">Опитайте отново</string>
|
<string name="retry">Опитайте отново</string>
|
||||||
<string name="genres">Жанрове</string>
|
<string name="genres">Жанрове</string>
|
||||||
<string name="director">Директор</string>
|
<string name="director">Режисьор</string>
|
||||||
<string name="writers">Писатели</string>
|
<string name="writers">Писатели</string>
|
||||||
<string name="seasons">Сезони</string>
|
<string name="seasons">Сезони</string>
|
||||||
<string name="play_button_description">Пуснете медията</string>
|
<string name="play_button_description">Пуснете медията</string>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
<string name="no_downloads">Вие нямате нищо свалено</string>
|
<string name="no_downloads">Вие нямате нищо свалено</string>
|
||||||
<string name="search">Търсене</string>
|
<string name="search">Търсене</string>
|
||||||
<string name="settings_category_language">Език</string>
|
<string name="settings_category_language">Език</string>
|
||||||
<string name="settings_preferred_audio_language">Предпочитан аудо език</string>
|
<string name="settings_preferred_audio_language">Предпочитан аудио език</string>
|
||||||
<string name="settings_preferred_subtitle_language">Предпочитан език на субтитрите</string>
|
<string name="settings_preferred_subtitle_language">Предпочитан език на субтитрите</string>
|
||||||
<string name="settings_category_servers">Сървъри</string>
|
<string name="settings_category_servers">Сървъри</string>
|
||||||
<string name="settings_category_player">Плейър</string>
|
<string name="settings_category_player">Плейър</string>
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
<string name="shows_label">ТВ Предавания</string>
|
<string name="shows_label">ТВ Предавания</string>
|
||||||
<string name="hide">Скрийте</string>
|
<string name="hide">Скрийте</string>
|
||||||
<string name="sort_order">Ред на сортиране</string>
|
<string name="sort_order">Ред на сортиране</string>
|
||||||
<string name="download_mobile_data">Свалете с мобилна дата</string>
|
<string name="download_mobile_data">Свалете с мобилни данни</string>
|
||||||
<string name="ascending">Възходящ</string>
|
<string name="ascending">Възходящ</string>
|
||||||
<string name="mpv_player">mpv плейър</string>
|
<string name="mpv_player">mpv плейър</string>
|
||||||
<string name="download_button_description">Свалете</string>
|
<string name="download_button_description">Свалете</string>
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<string name="error_preparing_player_items">Проблем при подготовката на елементите на плейъра.</string>
|
<string name="error_preparing_player_items">Проблем при подготовката на елементите на плейъра.</string>
|
||||||
<string name="view_details">Вижте детайли</string>
|
<string name="view_details">Вижте детайли</string>
|
||||||
<string name="view_details_underlined"><u>Вижте детайли</u></string>
|
<string name="view_details_underlined"><u>Вижте детайли</u></string>
|
||||||
<string name="about">За</string>
|
<string name="about">Описание</string>
|
||||||
<string name="privacy_policy">Политика за поверителност</string>
|
<string name="privacy_policy">Политика за поверителност</string>
|
||||||
<string name="app_info">Информация за приложението</string>
|
<string name="app_info">Информация за приложението</string>
|
||||||
<string name="unknown_error">Непозната грешка</string>
|
<string name="unknown_error">Непозната грешка</string>
|
||||||
|
@ -84,95 +84,110 @@
|
||||||
<string name="series_poster">Постер на сериала</string>
|
<string name="series_poster">Постер на сериала</string>
|
||||||
<string name="no_search_results">Няма резултати от търсенето</string>
|
<string name="no_search_results">Няма резултати от търсенето</string>
|
||||||
<string name="settings_category_download">Свалени</string>
|
<string name="settings_category_download">Свалени</string>
|
||||||
<string name="settings_use_cache_summary">Кеширайте снимките на диска за да ускорите времето за зареждане. Ще има ефект след рестартиране на приложението.</string>
|
<string name="settings_use_cache_summary">Кеширайте снимките на диска, за да ускорите времето за зареждане. Ще има ефект след рестартиране на приложението.</string>
|
||||||
<string name="settings_cache_size_message">Приложението ще използва това количество MB от вашето дисково пространство, за да съхранява изображения от сървъра на Jellyfin. По-големи стойности може да са от полза при по-бавни мрежи.</string>
|
<string name="settings_cache_size_message">Приложението ще използва това количество MB от вашето дисково пространство, за да съхранява изображения от сървъра на Jellyfin. По-големи стойности може да са от полза при по-бавни мрежи.</string>
|
||||||
<string name="descending">Низходящ</string>
|
<string name="descending">Низходящ</string>
|
||||||
<string name="track_selection">[%1$s] %2$s (%3$s)</string>
|
<string name="track_selection">[%1$s] %2$s (%3$s)</string>
|
||||||
<string name="runtime_minutes">%1$d мин</string>
|
<string name="runtime_minutes">%1$d мин</string>
|
||||||
<string name="size">Tamanho</string>
|
<string name="size">Размер</string>
|
||||||
<string name="seeking">Buscando</string>
|
<string name="seeking">Търсене</string>
|
||||||
<string name="settings_request_timeout">Tempo limite da solicitação (ms)</string>
|
<string name="settings_request_timeout">Лимит на заявките (ms)</string>
|
||||||
<string name="pref_player_mpv_ao">Saída de áudio</string>
|
<string name="pref_player_mpv_ao">Изход на аудиото</string>
|
||||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||||
<string name="image_description_backdrop">Pano de fundo de %1$s</string>
|
<string name="image_description_backdrop">Фон на %1$s</string>
|
||||||
<string name="users">Usuários</string>
|
<string name="users">Потребители</string>
|
||||||
<string name="no_server_connection">Sem conexão com o servidor Jellyfin, para assistir off-line, ative o modo off-line</string>
|
<string name="no_server_connection">Няма връзка към Jellyfin сървъра, за да гледате офлайн пуснете Режим без Интернет</string>
|
||||||
<string name="privacy_policy_notice">Ao usar o Findroid, você concorda com a <a href="https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY">Política de Privacidade</a>, que afirma que não coletamos quaisquer dados</string>
|
<string name="privacy_policy_notice">При ползването на Findroid, вие се съгласявате с <a href="https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY">Политиката за поверителност</a> , която гласи, че не събираме никаква информация</string>
|
||||||
<string name="app_description">Aplicativo Jellyfin nativo de terceiros</string>
|
<string name="app_description">Неофициално приложение за Jellyfin с \"native\" елементи</string>
|
||||||
<string name="dynamic_colors">Cores dinâmicas</string>
|
<string name="dynamic_colors">Динамични цветове</string>
|
||||||
<string name="person_detail_title">Detalhes</string>
|
<string name="person_detail_title">Детайли</string>
|
||||||
<string name="player_gestures_zoom_summary">Aperte para preencher a tela com o vídeo</string>
|
<string name="player_gestures_zoom_summary">Плъзнете с два пръста, за да изпълните екрана</string>
|
||||||
<string name="player_brightness_remember">Lembre-se do nível de brilho</string>
|
<string name="player_brightness_remember">Запомни ниво на яркост</string>
|
||||||
<string name="player_gestures_vb_summary">Deslize para cima e para baixo no lado direito da tela para alterar o volume e no lado esquerdo para alterar o brilho</string>
|
<string name="player_gestures_vb_summary">Плъзнете нагоре и надолу на дяснатата страна на екрана, за да промените звука, и на лявата страна, за да промените яркостта</string>
|
||||||
<string name="sort_by_options_1">Classificação IMDB</string>
|
<string name="sort_by_options_1">IMDB оценка</string>
|
||||||
<string name="sort_by_options_3">data adicionada</string>
|
<string name="sort_by_options_3">Дата Добавен</string>
|
||||||
<string name="seek_back_increment">Buscar incremento de volta (ms)</string>
|
<string name="seek_back_increment">Инкремент за връщане назад(ms)</string>
|
||||||
<string name="pref_player_mpv_vo">Saida de video</string>
|
<string name="pref_player_mpv_vo">Изхода на видеото</string>
|
||||||
<string name="pref_player_intro_skipper_summary">Requer que o plugin Confused Polar Bears Intro Skipper esteja instalado no servidor</string>
|
<string name="pref_player_intro_skipper_summary">Изисква Confused Polar Bears Intro Skipper да бъде инсталиран на сървъра</string>
|
||||||
<string name="remove_user">Remover usuário</string>
|
<string name="remove_user">Премахване на потребител</string>
|
||||||
<string name="remove_user_dialog_text">Tem certeza de que deseja remover o usuário %1$s</string>
|
<string name="remove_user_dialog_text">Сигурни ли сте, че искате да премахнете потребител %1$s</string>
|
||||||
<string name="quick_connect">Conexão rápida</string>
|
<string name="quick_connect">Бързо Свързване</string>
|
||||||
<string name="extra_info_summary">Exibe informações detalhadas sobre áudio, vídeo e legendas</string>
|
<string name="extra_info_summary">Показва детайлна информация за Аудио, Видео и Субтитри</string>
|
||||||
<string name="subtitle_chip_text">CC</string>
|
<string name="subtitle_chip_text">Затворени субтитри</string>
|
||||||
<string name="temp">Temperatura</string>
|
<string name="temp">временно</string>
|
||||||
<string name="offline_mode_icon">Ícone do modo off-line</string>
|
<string name="offline_mode_icon">Режим без Интернет иконка</string>
|
||||||
<string name="offline_mode_go_online">Fique online</string>
|
<string name="offline_mode_go_online">Свържи се с Интернет</string>
|
||||||
<string name="select_storage_location">Selecione o local de armazenamento</string>
|
<string name="select_storage_location">Изберете локация на диска</string>
|
||||||
<string name="theme_light">Luz</string>
|
<string name="theme_light">Светла</string>
|
||||||
<string name="amoled_theme_summary">Use tema AMOLED com fundo preto puro</string>
|
<string name="amoled_theme_summary">Използвайте AMOLED тема с чисто черен фон</string>
|
||||||
<string name="theme_system">Siga o sistema</string>
|
<string name="theme_system">Следвай системата</string>
|
||||||
<string name="libraries">Bibliotecas</string>
|
<string name="libraries">Библиотеки</string>
|
||||||
<string name="amoled_theme">Tema escuro AMOLED</string>
|
<string name="amoled_theme">AMOLED тъмна тема</string>
|
||||||
<string name="player_gestures_seek">Procure gesto</string>
|
<string name="player_gestures_seek">Жест за търсене</string>
|
||||||
<string name="player_gestures_seek_summary">Deslize horizontalmente para avançar ou retroceder</string>
|
<string name="player_gestures_seek_summary">Плъзнете хоризонтално, за да върнете назад или да пропуснете напред</string>
|
||||||
<string name="downloaded_indicator">Indicador baixado</string>
|
<string name="downloaded_indicator">Индикатор за сваленост</string>
|
||||||
<string name="app_language">Idioma do aplicativo</string>
|
<string name="app_language">Език на приложението</string>
|
||||||
<string name="episodes_label">Episódios</string>
|
<string name="episodes_label">Епизоди</string>
|
||||||
<string name="image_description_poster">pôster de %1$s</string>
|
<string name="image_description_poster">Постер на %1$s</string>
|
||||||
<string name="player_gestures">Gestos do jogador</string>
|
<string name="player_gestures">Жестове на плейъра</string>
|
||||||
<string name="player_gestures_vb">Gestos de volume e brilho</string>
|
<string name="player_gestures_vb">Жестове за яркост и звук</string>
|
||||||
<string name="sort_by_options_0">Título</string>
|
<string name="sort_by_options_0">Заглавие</string>
|
||||||
<string name="dynamic_colors_summary">Use cores dinâmicas do Material You (disponível apenas no Android 12+)</string>
|
<string name="dynamic_colors_summary">Използвай динамични Material You цветове (възможно единствено на Android 12+)</string>
|
||||||
<string name="sort_by_options_2">Avaliação parental</string>
|
<string name="sort_by_options_2">Оценка за родителски контрол</string>
|
||||||
<string name="sort_by_options_4">Data de reprodução</string>
|
<string name="sort_by_options_4">Дата пускан</string>
|
||||||
<string name="seek_forward_increment">Procure incremento direto (ms)</string>
|
<string name="seek_forward_increment">Инкремент за пропускане напред(ms)</string>
|
||||||
<string name="subtitles">Legendas</string>
|
<string name="subtitles">Субтитри</string>
|
||||||
<string name="subtitles_summary">Personalize a aparência das legendas</string>
|
<string name="subtitles_summary">Персонализирайте външния вид на субтитрите</string>
|
||||||
<string name="settings_category_network">Rede</string>
|
<string name="settings_category_network">Мрежа</string>
|
||||||
<string name="settings_socket_timeout">Tempo limite do soquete (ms)</string>
|
<string name="settings_socket_timeout">Лимит на сокета (ms)</string>
|
||||||
<string name="pref_player_mpv_hwdec">Decodificação de hardware</string>
|
<string name="pref_player_mpv_hwdec">Хардуерно декодиране</string>
|
||||||
<string name="pref_player_trick_play_summary">Requer que o plugin Jellyscrub do Nicknsy esteja instalado no servidor</string>
|
<string name="add_address">Добави адрес</string>
|
||||||
<string name="add_address">Adicionar endereço</string>
|
<string name="audio">Аудио</string>
|
||||||
<string name="audio">Audio</string>
|
<string name="video">Видео</string>
|
||||||
<string name="video">Vídeo</string>
|
<string name="extra_info">Покажи Допълнителна информация</string>
|
||||||
<string name="extra_info">Exibir informações extras</string>
|
<string name="external">Външно</string>
|
||||||
<string name="external">Externo</string>
|
<string name="storage_name">%1$s (%2$d MB свободни)</string>
|
||||||
<string name="storage_name">%1$s (%2$d MB grátis)</string>
|
<string name="cancel_download">Отмени свалянето</string>
|
||||||
<string name="cancel_download">Cancelar transferência</string>
|
<string name="preparing_download">Подготвяне на свалянето</string>
|
||||||
<string name="preparing_download">Preparando transferência</string>
|
<string name="cancel_download_message">Сигурни ли сте, че искате на отмените свалянето?</string>
|
||||||
<string name="cancel_download_message">Tem certeza de que deseja cancelar a transferência\?</string>
|
<string name="stop_download">Спри свалянето</string>
|
||||||
<string name="stop_download">Pare de baixar</string>
|
<string name="storage_unavailable">Локацията на диска е недостъпна</string>
|
||||||
<string name="storage_unavailable">O local de armazenamento não está disponível</string>
|
<string name="internal">Вътрежно</string>
|
||||||
<string name="internal">Interno</string>
|
<string name="remove_server_address">Премахване на адрес на сървър</string>
|
||||||
<string name="remove_server_address">Remover endereço do servidor</string>
|
<string name="remove_server_address_dialog_text">Сигурни ли сте, че искате да премахнете адреса на сървъра %1$s</string>
|
||||||
<string name="remove_server_address_dialog_text">Tem certeza de que deseja remover o endereço do servidor %1$s</string>
|
<string name="sort_by_options_5">Дата на издаване</string>
|
||||||
<string name="sort_by_options_5">Data de lançamento</string>
|
<string name="select_video_version_title">Изберете версия</string>
|
||||||
<string name="select_video_version_title">Selecione a versão</string>
|
<string name="theme_dark">Тъмна</string>
|
||||||
<string name="theme_dark">Escuro</string>
|
<string name="pref_player_intro_skipper">Пропускане на интрота</string>
|
||||||
<string name="pref_player_intro_skipper">Capitão de introdução</string>
|
<string name="addresses">Адреси</string>
|
||||||
<string name="pref_player_trick_play">Truque</string>
|
<string name="add_server_address">Добави адрес на сървър</string>
|
||||||
<string name="addresses">Endereços</string>
|
<string name="player_gestures_zoom">Жест за приближаване (zoom)</string>
|
||||||
<string name="add_server_address">Adicionar endereço do servidor</string>
|
<string name="settings_connect_timeout">Лимит на свързването (ms)</string>
|
||||||
<string name="player_gestures_zoom">Gesto de zoom</string>
|
<string name="add_user">Добави потребител</string>
|
||||||
<string name="settings_connect_timeout">Tempo limite de conexão (ms)</string>
|
<string name="add">Добави</string>
|
||||||
<string name="add_user">Adicionar usuário</string>
|
<string name="picture_in_picture">Режим Картина-в-картина</string>
|
||||||
<string name="add">Adicionar</string>
|
<string name="picture_in_picture_gesture">Жест за Режим Картина-в-картина</string>
|
||||||
<string name="picture_in_picture">Imagem em imagem</string>
|
<string name="picture_in_picture_gesture_summary">Използайте Home бутона или жест, за да влезете в режим Картина-в-картина</string>
|
||||||
<string name="picture_in_picture_gesture">Gesto inicial picture-in-picture</string>
|
<string name="subtitle">Субтитри</string>
|
||||||
<string name="picture_in_picture_gesture_summary">Use o botão home ou gesto para entrar picture-in-picture enquanto o vídeo está sendo reproduzido</string>
|
<string name="offline_mode">Режим без Интернет</string>
|
||||||
<string name="subtitle">Legendas</string>
|
<string name="downloading_error">Грешка при сваляне</string>
|
||||||
<string name="offline_mode">Modo offline</string>
|
<string name="not_enough_storage">Тази медия изисква %1$s свободно място, но има само %2$s свободно</string>
|
||||||
<string name="downloading_error">Erro ao baixar</string>
|
<string name="remove_from_favorites">Махни от любими</string>
|
||||||
<string name="not_enough_storage">Este item requer %1$s de armazenamento gratuito, mas apenas %2$s está disponível</string>
|
<string name="collection_no_media">Тази колекция не съдържа никаква медия</string>
|
||||||
|
<string name="player_start_maximized">Започни в максимизиран режим</string>
|
||||||
|
<string name="player_start_maximized_summary">Отвори видео в максимизиран режим по подразбиране</string>
|
||||||
|
<string name="player_gestures_chapter_skip_summary">Дълго задържане на Лявата / Дясната страна, за пропускане на глава (отменя жеста за 2x скорост)</string>
|
||||||
|
<string name="pref_player_chapter_markers">Маркери за глави</string>
|
||||||
|
<string name="pref_player_chapter_markers_summary">Показвай маркери за глави на времевата линия</string>
|
||||||
|
<string name="no_servers_found">Не са намерени сървъри</string>
|
||||||
|
<string name="no_users_found">Не са намерени потребители</string>
|
||||||
|
<string name="select_user">Избери потребител</string>
|
||||||
|
<string name="live_tv">Телевизия На Живо</string>
|
||||||
|
<string name="play">Пусни</string>
|
||||||
|
<string name="mark_as_played">Маркирай като гледано</string>
|
||||||
|
<string name="unmark_as_played">Махни маркирано като гледано</string>
|
||||||
|
<string name="watch_trailer">Гледай трейлър</string>
|
||||||
|
<string name="add_to_favorites">Добави към любими</string>
|
||||||
|
<string name="player_gestures_chapter_skip">Пропусни глава</string>
|
||||||
</resources>
|
</resources>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue