Compare commits

...

58 Commits

Author SHA1 Message Date
Niels van Velzen
a257651f8e Remove refreshing of Live TV program data in player 2024-05-28 16:34:45 +02:00
Niels van Velzen
0690c1c898 Use singleTask launch mode for StartupActivity
(cherry picked from commit 44a4cfec0c)
2024-05-28 16:06:42 +02:00
Niels van Velzen
66ec39f566 Support new intent in StartupActivity
(cherry picked from commit 879831d601)
2024-05-28 16:06:42 +02:00
Niels van Velzen
f4abd1246d Add app notification to notify about upcoming server requirement
(cherry picked from commit dd5d6e7b3e)
2024-05-26 22:06:52 +02:00
Niels van Velzen
a17821181f Force Quick Connect code to use LTR text direction
(cherry picked from commit 73626e185e)
2024-05-26 22:06:46 +02:00
Niels van Velzen
4da68f86cb Fix "Add server" button not visible when there are too many servers
(cherry picked from commit 5d0a677265)
2024-05-26 22:06:38 +02:00
Niels van Velzen
487bf3b7ee Revert "fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.3.1+1"
This reverts commit 6031979f4c.
2024-04-24 20:48:02 +02:00
Niels van Velzen
21f5256e0f Revert "Fix channel adapter not using string resource"
This reverts commit 9c068290bb.
2024-04-18 22:24:03 +02:00
Niels van Velzen
41df9a59df Fix getAllChannelsIndex always returning -1
(cherry picked from commit a086a2d241)
2024-04-18 22:18:37 +02:00
Niels van Velzen
3709bf5f5a Fix channel adapter not using string resource
(cherry picked from commit 9c068290bb)
2024-04-18 22:18:00 +02:00
Niels van Velzen
3cdc1cdaba Fix Live TV channels refreshing to infinity
(cherry picked from commit 17f16208fd)
2024-04-18 22:17:57 +02:00
renovate[bot]
6031979f4c fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.3.1+1
(cherry picked from commit 94ca41bae0)
2024-04-18 22:16:34 +02:00
renovate[bot]
896f66254f fix(deps): update androidx.media3 to v1.3.1
(cherry picked from commit e36268712b)
2024-04-18 22:16:33 +02:00
Niels van Velzen
3a18933b87 USe Timber.e instead of Exception.printStackTrace
(cherry picked from commit 9f90bf7c33)
2024-04-08 17:01:16 +02:00
Niels van Velzen
8c45084406 Update SDK to 1.4.7 (10.8 with enum backports) 2024-04-07 20:34:56 +02:00
Niels van Velzen
d4a297710b Fix app_logo cutoff in some cases
(cherry picked from commit dbde98ac50)
2024-04-07 10:32:11 +02:00
Niels van Velzen
19f30c19b0 Check if fragment is null in PlaybackController.play (#3475)
* Check if fragment is null in PlaybackController.play

* Update logging levels in PlaybackController

(cherry picked from commit d22ab694e2)
2024-04-07 10:32:10 +02:00
Niels van Velzen
4e8e8bc232 Fix search action ignoring received query
(cherry picked from commit 2784a1bcbb)
2024-03-12 15:20:31 +01:00
Niels van Velzen
ec5c751390 Update logo sizing in splash screen and screensaver
(cherry picked from commit b903bc0897)
2024-03-11 19:33:37 +01:00
Niels van Velzen
46b0b7aade Update app_logo to use new logo font
(cherry picked from commit 47282c09b0)
2024-03-11 19:33:36 +01:00
Niels van Velzen
3877739320 Fix background color for preference list views
(cherry picked from commit dd5002beac)
2024-03-11 19:33:36 +01:00
Niels van Velzen
538eeb4f22 Recreate icon/banner resources
(cherry picked from commit 07692d83b2)
2024-03-11 19:33:36 +01:00
renovate[bot]
0d724c532a fix(deps): update androidx.media3 to v1.3.0
(cherry picked from commit f2e802a300)
2024-03-11 19:33:29 +01:00
Niels van Velzen
eba14e0bf4 Use fully transparent background during video playback
(cherry picked from commit 60e694959c)
2024-03-11 19:32:59 +01:00
Joshua M. Boniface
a8c35b0ea0 Remove a target dir if it exists
Helps with potential tag overwrites should they occur.

(cherry picked from commit 07a3ef431a)
2024-03-11 19:32:59 +01:00
Joshua M. Boniface
17744729b2 Fix paths to use incoming and sudo move
Aligns with new permissions model on new repo server.

(cherry picked from commit eba5ef0e03)
2024-03-11 19:32:58 +01:00
Joshua M. Boniface
fd5442097e Update CI to new repository secrets and layout
(cherry picked from commit 6556c4c02e)
2024-03-11 19:32:35 +01:00
Niels van Velzen
f9eeae6bf0 Fix crash when checking whether a person is a folder or not
(cherry picked from commit efc6be8d3d)
2024-02-25 13:25:58 +01:00
Niels van Velzen
c2e33b9b13 Fix crash in SocketHandler.onPlayMessage when user id is not set
(cherry picked from commit 8b87dafb26)
2024-02-25 13:25:58 +01:00
Niels van Velzen
9c4b121574 Fix DPAD_CENTER on screensaver not handled
(cherry picked from commit fb25e79233)
2024-02-25 13:25:58 +01:00
Niels van Velzen
ddb1be18bc Replace CoroutineUtils.runBlocking with new CoroutineUtils.runOnLifecycle
This will prevent the main thread from blocking, which in extreme cases can kill the app

(cherry picked from commit bd704c4ac9)
2024-02-25 13:25:57 +01:00
Niels van Velzen
4a4c1ed6ca Fix crash when updating clock in FullDetailsFragment while item information is unavailable
(cherry picked from commit 68b578057d)
2024-02-25 13:25:57 +01:00
Niels van Velzen
c28759e165 Update app/src/main/java/org/jellyfin/androidtv/util/profile/MediaCodecCapabilitiesTest.kt
(cherry picked from commit d466420858)
2024-02-18 09:57:52 +01:00
lawadr
45a5adbc0b Fix up
(cherry picked from commit f5125e937a)
2024-02-18 09:57:42 +01:00
Niels van Velzen
118eacfa90 Specify BUILD_TOOLS_VERSION for r0adkll/sign-android-release step in publish workflow 2024-02-13 13:45:10 +01:00
MichaelR416
91ccf32f77 Fix media details tag for multiple versions support
(cherry picked from commit 9ec9de0ba1)
2024-02-11 16:14:29 +01:00
Niels van Velzen
065e738773 Only show PG-13 content in screensaver
(cherry picked from commit 5ff02c7e5d)
2024-02-11 16:12:35 +01:00
MichaelR416
65a7bbf501 Add if else statement for audio downmixing support
(cherry picked from commit d5c5bcc41c)
2024-02-11 16:12:30 +01:00
renovate[bot]
d661c2faf3 fix(deps): update dependency org.jellyfin.media3:media3-ffmpeg-decoder to v1.2.1+1
(cherry picked from commit 60eac66d90)
2024-02-11 16:12:14 +01:00
renovate[bot]
8175dbd1c0 fix(deps): update androidx.media3 to v1.2.1
(cherry picked from commit abf46d42f5)
2024-02-11 16:12:13 +01:00
lawadr
f068be6945 Widen support for HEVC Main 10
Previously the HEVC codec profile only flagged support for HEVC Main 10
when MediaCodec advertised its support for level 5 (main tier) or above.
This meant devices that support only levels 4 or 4.1 (such as the
Chromecast with Google TV HD) would transcode HEVC Main 10 content even
when supported.

This change lowers the requirement from level 5 to level 4 and also adds
additional profiles specifically for HEVC Main and Main 10 with
conditions on the level so that levels beyond what is supported by
MediaCodec get transcoded.

The result is that HEVC Main 10 level 5 videos get transcoded on devices
supporting only 4.1, but HEVC Main 10 level 4.1 content plays back
directly.

(cherry picked from commit 1fb3e0399c)
2024-02-11 16:11:41 +01:00
MichaelR416
fe465db020 Use LTR for default subtitle direction
(cherry picked from commit 17b76e1536)
2024-02-11 16:11:36 +01:00
Gwilherm Folliot
d6fe265b38 Fix crash when quitting player after two plays
If you play a title from a device and play another without quitting the player and then quit the player the app crash.

Its because the NavigationRepository stacks two players and when coming to the last one, the app crash because the last player is terminated.

My fix just replace the last destination if an fragment is active

java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.activity.OnBackPressedDispatcher.addCallback, parameter onBackPressedCallback
	at androidx.activity.OnBackPressedDispatcher.addCallback(Unknown Source:3)
	at org.jellyfin.androidtv.ui.playback.CustomPlaybackOverlayFragment.onActivityCreated(CustomPlaybackOverlayFragment.java:346)
	at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:3156)
	at androidx.fragment.app.FragmentStateManager.activityCreated(FragmentStateManager.java:619)
	at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:275)
	at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1934)
	at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1839)
	at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1782)
	at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:565)
	at android.os.Handler.handleCallback(Handler.java:942)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loopOnce(Looper.java:201)
	at android.os.Looper.loop(Looper.java:288)
	at android.app.ActivityThread.main(ActivityThread.java:7884)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

(cherry picked from commit 6867c319bb)
2024-02-11 16:11:29 +01:00
Niels van Velzen
8eecfab5d7 Disable tunneling 2024-01-07 14:53:05 +01:00
Niels van Velzen
0f9ffcacb9 Defer home initialization until user info is available
(cherry picked from commit d721482127)
2024-01-06 13:50:56 +01:00
Niels van Velzen
aa7c45d462 Disable crossfading for CardLegacyImageView to fix scaling issue
(cherry picked from commit 1fbc19740b)
2024-01-05 19:54:19 +01:00
Gwilherm Folliot
156f5ecd3c Fix crash when DISPLAY_CONTENT while playing
When a video is playing and on another device you browse libraries on a casting device it cause the app to crash

The onDisplayContent method is trying to read ExoPlayer state but from the socket thread and not the main thread

java.lang.IllegalStateException: Player is accessed on the wrong thread.
Current thread: 'DefaultDispatcher-worker-4'
Expected thread: 'main'
See https://developer.android.com/guide/topics/media/issues/player-accessed-on-wrong-thread
	at androidx.media3.exoplayer.ExoPlayerImpl.verifyApplicationThread(ExoPlayerImpl.java:2842)
	at androidx.media3.exoplayer.ExoPlayerImpl.getPlaybackState(ExoPlayerImpl.java:514)
	at androidx.media3.common.BasePlayer.isPlaying(BasePlayer.java:124)
	at org.jellyfin.androidtv.ui.playback.VideoManager.isPlaying(VideoManager.java:341)
	at org.jellyfin.androidtv.ui.playback.PlaybackController.isPlaying(PlaybackController.java:262)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler.onDisplayContent(SocketHandler.kt:184)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler.lambda$13$lambda$8(SocketHandler.kt:103)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler$$ExternalSyntheticLambda3.onReceive(Unknown Source:4)
	at org.jellyfin.androidtv.data.eventhandling.SocketHandler$_init_$lambda$13$$inlined$addGeneralCommandsListener$default$1.onReceive(ListenerRegistrationExtensions.kt:68)
	at org.jellyfin.sdk.api.sockets.helper.ListenerHelper.forwardMessage(ListenerHelper.kt:37)
	at org.jellyfin.sdk.api.sockets.SocketInstance.forwardMessage(SocketInstance.kt:283)
	at org.jellyfin.sdk.api.sockets.SocketInstance.access$forwardMessage(SocketInstance.kt:35)
	at org.jellyfin.sdk.api.sockets.SocketInstance$connectAndBind$2.invokeSuspend(SocketInstance.kt:220)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)

(cherry picked from commit 0116530794)
2024-01-05 19:54:18 +01:00
Niels van Velzen
e56fd0d024 Set TsExtractor TimestampSearchBytes parameter based on device memory capabilities
(cherry picked from commit 76acd17d81)
2024-01-05 19:54:18 +01:00
Niels van Velzen
06e245fe48 Enable tunneling in VideoManager
(cherry picked from commit e646192196)
2024-01-05 19:54:17 +01:00
Niels van Velzen
9f0fa644a7 Enable tunneling in ExoPlayerBackend
(cherry picked from commit 1f0345f16f)
2024-01-05 19:54:17 +01:00
Niels van Velzen
5b213199dd Add <queries> element to Manifest for remote trailers
(cherry picked from commit b91fd1a0f5)
2024-01-05 19:54:16 +01:00
Niels van Velzen
b4f6273ede Disable SocketHandler when using RewriteMediaManager
(cherry picked from commit 332b6af3da)
2024-01-05 19:54:16 +01:00
Niels van Velzen
66c624e16c Replace ExoPlayer with androidx.media3
(cherry picked from commit bd204fdf87)
2024-01-05 19:54:15 +01:00
Niels van Velzen
e47916f1c2 Fix crash and inconsistent ordering when using ShuffleOrderIndexProvider
(cherry picked from commit b9e19b2515)
2024-01-05 19:54:15 +01:00
Niels van Velzen
ab7875cbb3 Fix opening collections via search results showing empty screen
(cherry picked from commit 2bbbb93dbf)
2024-01-05 19:54:14 +01:00
Niels van Velzen
5f15615a96 Fix playing audio from search queues entire row
(cherry picked from commit 8b43402192)
2024-01-05 19:54:14 +01:00
Niels van Velzen
9cda86ab84 Fix focus issue when pressing back in Fire TV soft keyboard
(cherry picked from commit c70f814064)
2024-01-05 19:54:14 +01:00
Niels van Velzen
1873497fec Fix RewriteMediaManager never used in production builds
(cherry picked from commit 75b6b12147)
2023-12-18 22:21:02 +01:00
98 changed files with 949 additions and 461 deletions

View File

@@ -27,6 +27,8 @@ jobs:
- name: Sign APK
id: signApk
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
env:
BUILD_TOOLS_VERSION: "34.0.0"
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
@@ -36,6 +38,8 @@ jobs:
- name: Sign app bundle
id: signAab
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # tag=v1
env:
BUILD_TOOLS_VERSION: "34.0.0"
with:
releaseDirectory: app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
@@ -61,20 +65,24 @@ jobs:
with:
switches: -vrptz
path: build/jellyfin-publish/
remote_path: /srv/repository/releases/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
remote_path: /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }}/
remote_host: ${{ secrets.REPO_HOST }}
remote_user: ${{ secrets.REPO_USER }}
remote_key: ${{ secrets.REPO_KEY }}
- name: Update repo.jellyfin.org symlinks
uses: appleboy/ssh-action@55dabf81b49d4120609345970c91507e2d734799 # v1.0.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
host: ${{ secrets.REPO_HOST }}
username: ${{ secrets.REPO_USER }}
key: ${{ secrets.REPO_KEY }}
envs: JELLYFIN_VERSION
script_stop: true
script: |
cd /srv/repository/releases/client/androidtv;
rm -rf *.apk version.txt;
ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
ln -s versions/v${JELLYFIN_VERSION}/version.txt .;
if [ -d "/srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}" ] && [ -n "${{ env.JELLYFIN_VERSION }}" ]; then
sudo rm -r /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
fi
sudo mv /srv/incoming/androidtv/v${{ env.JELLYFIN_VERSION }} /srv/repository/main/client/androidtv/versions/v${{ env.JELLYFIN_VERSION }}
cd /srv/repository/main/client/androidtv;
sudo rm -rf *.apk version.txt;
sudo ln -s versions/v${JELLYFIN_VERSION}/jellyfin-androidtv-v${JELLYFIN_VERSION}-*.apk .;
sudo ln -s versions/v${JELLYFIN_VERSION}/version.txt .;

View File

@@ -12,6 +12,7 @@
- [GodTamIt](https://github.com/GodTamIt)
- [sparky3387](https://github.com/sparky3387)
- [mohd-akram](https://github.com/mohd-akram)
- [3l0w](https://github.com/3l0w)
# Emby Contributors

View File

@@ -139,8 +139,10 @@ dependencies {
implementation(libs.bundles.koin)
// Media players
implementation(libs.exoplayer)
implementation(libs.jellyfin.exoplayer.ffmpegextension)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.ui)
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
implementation(libs.libvlc)
// Markdown

View File

@@ -28,17 +28,25 @@
android:name="android.hardware.microphone"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data
android:host="youtube.com"
android:scheme="https" />
</intent>
</queries>
<application
android:name=".JellyfinApplication"
android:allowBackup="true"
android:appCategory="video"
android:banner="@drawable/app_banner"
android:banner="@mipmap/app_banner"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="@xml/backup_content"
android:icon="@drawable/app_icon"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@drawable/app_icon_round"
android:supportsRtl="true"
android:theme="@style/Theme.Jellyfin"
android:usesCleartextTraffic="true">
@@ -101,6 +109,7 @@
android:name=".ui.startup.StartupActivity"
android:exported="true"
android:noHistory="true"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing">
<intent-filter>

View File

@@ -22,6 +22,7 @@ import org.jellyfin.sdk.api.client.extensions.brandingApi
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.model.ServerVersion
import org.jellyfin.sdk.model.api.BrandingOptions
import org.jellyfin.sdk.model.api.ServerDiscoveryInfo
import org.jellyfin.sdk.model.serializer.toUUID
@@ -47,6 +48,8 @@ interface ServerRepository {
companion object {
val minimumServerVersion = Jellyfin.minimumVersion.copy(build = null)
val recommendedServerVersion = Jellyfin.apiVersion.copy(build = null)
val upcomingMinimumServerVersion = ServerVersion(10, 9, 0)
}
}

View File

@@ -14,6 +14,7 @@ import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.playback.MediaManager
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer
import org.jellyfin.androidtv.ui.playback.rewrite.RewriteMediaManager
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
@@ -126,47 +127,65 @@ class SocketHandler(
private fun onPlayMessage(message: PlayMessage) {
val itemId = message.request.itemIds?.firstOrNull() ?: return
PlaybackHelper.retrieveAndPlay(
itemId.toString(),
false,
message.request.startPositionTicks,
context
)
runCatching {
PlaybackHelper.retrieveAndPlay(
itemId.toString(),
false,
message.request.startPositionTicks,
context
)
}.onFailure { Timber.w(it, "Failed to start playback") }
}
@Suppress("ComplexMethod")
private fun onPlayStateMessage(message: PlayStateMessage) = coroutineScope.launch(Dispatchers.Main) {
Timber.i("Received PlayStateMessage with command ${message.request.command}")
val playbackController = playbackControllerContainer.playbackController
// Audio playback uses the mediaManager, video playback and live tv use the playbackController
if (mediaManager.isAudioPlayerInitialized) when (message.request.command) {
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
// Not implemented
PlaystateCommand.SEEK,
PlaystateCommand.REWIND,
PlaystateCommand.FAST_FORWARD -> Unit
} else when (message.request.command) {
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
PlaystateCommand.SEEK -> playbackController?.seek(
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
)
PlaystateCommand.REWIND -> playbackController?.rewind()
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
// Audio playback uses (Rewrite)MediaManager, (legacy) video playback uses playbackController
when {
// Ignore RewriteMediaManager
mediaManager is RewriteMediaManager && mediaManager.hasAudioQueueItems() -> {
Timber.i("Ignoring PlayStateMessage: should be handled by PlaySessionSocketService")
return@launch
}
// LegacyMediaManager
mediaManager.hasAudioQueueItems() -> when (message.request.command) {
PlaystateCommand.STOP -> mediaManager.stopAudio(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> mediaManager.playPauseAudio()
PlaystateCommand.NEXT_TRACK -> mediaManager.nextAudioItem()
PlaystateCommand.PREVIOUS_TRACK -> mediaManager.prevAudioItem()
// Not implemented
PlaystateCommand.SEEK,
PlaystateCommand.REWIND,
PlaystateCommand.FAST_FORWARD -> Unit
}
// PlaybackController
else -> {
val playbackController = playbackControllerContainer.playbackController
when (message.request.command) {
PlaystateCommand.STOP -> playbackController?.endPlayback(true)
PlaystateCommand.PAUSE, PlaystateCommand.UNPAUSE, PlaystateCommand.PLAY_PAUSE -> playbackController?.playPause()
PlaystateCommand.NEXT_TRACK -> playbackController?.next()
PlaystateCommand.PREVIOUS_TRACK -> playbackController?.prev()
PlaystateCommand.SEEK -> playbackController?.seek(
(message.request.seekPositionTicks ?: 0) / TICKS_TO_MS
)
PlaystateCommand.REWIND -> playbackController?.rewind()
PlaystateCommand.FAST_FORWARD -> playbackController?.fastForward()
}
}
}
}
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) {
private fun onDisplayContent(itemId: UUID, itemKind: BaseItemKind) = coroutineScope.launch(Dispatchers.Main) {
val playbackController = playbackControllerContainer.playbackController
if (playbackController?.isPlaying == true || playbackController?.isPaused == true) {
Timber.i("Not launching $itemId: playback in progress")
return
return@launch
}
Timber.i("Launching $itemId")

View File

@@ -6,15 +6,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jellyfin.androidtv.BuildConfig
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.model.Server
import org.jellyfin.androidtv.auth.repository.ServerRepository
import org.jellyfin.androidtv.data.model.AppNotification
import org.jellyfin.androidtv.preference.SystemPreferences
import org.jellyfin.androidtv.util.isTvDevice
import org.jellyfin.sdk.model.ServerVersion
interface NotificationsRepository {
val notifications: StateFlow<List<AppNotification>>
fun dismissNotification(item: AppNotification)
fun addDefaultNotifications()
fun updateServerNotifications(server: Server?)
}
class NotificationsRepositoryImpl(
@@ -34,15 +38,28 @@ class NotificationsRepositoryImpl(
addBetaNotification()
}
private fun addNotification(message: String, public: Boolean = false, dismiss: () -> Unit = {}) {
notifications.value = notifications.value + AppNotification(message, dismiss, public)
private fun addNotification(
message: String,
public: Boolean = false,
dismiss: () -> Unit = {}
): AppNotification {
val notification = AppNotification(message, dismiss, public)
notifications.value += notification
return notification
}
private fun removeNotification(notification: AppNotification) {
notifications.value -= notification
}
private fun addUiModeNotification() {
val disableUiModeWarning = systemPreferences[SystemPreferences.disableUiModeWarning]
if (!context.isTvDevice() && !disableUiModeWarning) {
addNotification(context.getString(R.string.app_notification_uimode_invalid), public = true)
addNotification(
context.getString(R.string.app_notification_uimode_invalid),
public = true
)
}
}
@@ -53,8 +70,28 @@ class NotificationsRepositoryImpl(
if (isBeta && currentVersion != dismissedVersion) {
addNotification(context.getString(R.string.app_notification_beta, currentVersion)) {
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] = currentVersion
systemPreferences[SystemPreferences.dismissedBetaNotificationVersion] =
currentVersion
}
}
}
// Update server notification
private var _updateServerNotification: AppNotification? = null
override fun updateServerNotifications(server: Server?) {
// Remove current update notification
_updateServerNotification?.let(::removeNotification)
val currentServerVersion = server?.version?.let(ServerVersion::fromString) ?: return
if (currentServerVersion < ServerRepository.upcomingMinimumServerVersion) {
_updateServerNotification =
addNotification(
message = context.getString(
R.string.app_notification_update_soon,
currentServerVersion,
ServerRepository.upcomingMinimumServerVersion
),
)
}
}
}

View File

@@ -47,7 +47,9 @@ class BackgroundService(
private var _backgrounds = emptyList<ImageBitmap>()
private var _currentIndex = 0
private var _currentBackground = MutableStateFlow<ImageBitmap?>(null)
private var _enabled = MutableStateFlow(true)
val currentBackground get() = _currentBackground.asStateFlow()
val enabled get() = _enabled.asStateFlow()
// Helper function for [setBackground]
private fun List<String>?.getUrls(itemId: UUID?): List<String> {
@@ -104,6 +106,9 @@ class BackgroundService(
private fun loadBackgrounds(backdropUrls: Set<String>) {
if (backdropUrls.isEmpty()) return clearBackgrounds()
// Re-enable backgrounds if disabled
_enabled.value = true
// Cancel current loading job
loadBackgroundsJob?.cancel()
loadBackgroundsJob = scope.launch(Dispatchers.IO) {
@@ -122,12 +127,22 @@ class BackgroundService(
fun clearBackgrounds() {
loadBackgroundsJob?.cancel()
// Re-enable backgrounds if disabled
_enabled.value = true
if (_backgrounds.isEmpty()) return
_backgrounds = emptyList()
update()
}
/**
* Disable the showing of backgrounds until any function manipulating the backgrounds is called.
*/
fun disable() {
_enabled.value = false
}
internal fun update() {
val now = System.currentTimeMillis()
if (lastBackgroundTimerUpdate > now - TRANSITION_DURATION.inWholeMilliseconds)

View File

@@ -35,7 +35,7 @@ val playbackModule = module {
factory {
val preferences = get<UserPreferences>()
val useRewrite = preferences[UserPreferences.playbackRewriteAudioEnabled] && BuildConfig.DEVELOPMENT
val useRewrite = preferences[UserPreferences.playbackRewriteAudioEnabled]
if (useRewrite) get<RewriteMediaManager>()
else get<LegacyMediaManager>()

View File

@@ -198,7 +198,7 @@ class LeanbackChannelWorker(
}
// Update logo
ResourcesCompat.getDrawable(context.resources, R.drawable.app_icon, context.theme)?.let {
ResourcesCompat.getDrawable(context.resources, R.mipmap.app_icon, context.theme)?.let {
ChannelLogoUtils.storeChannelLogo(
context,
ContentUris.parseId(uri),

View File

@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.integration.dream.composable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@@ -26,5 +27,6 @@ fun DreamContentLogo() = Box(
modifier = Modifier
.align(Alignment.Center)
.width(400.dp)
.fillMaxHeight()
)
}

View File

@@ -76,6 +76,9 @@ private suspend fun getRandomLibraryShowcase(
sortBy = listOf(ItemSortBy.Random),
limit = 5,
imageTypes = listOf(ImageType.BACKDROP),
// TODO: Add preferences for these two settings
maxOfficialRating = "PG-13",
// hasParentalRating = true,
)
val item = response.items?.firstOrNull { item ->

View File

@@ -82,7 +82,10 @@ class AsyncImageView @JvmOverloads constructor(
}.build())
} else {
imageLoader.enqueue(ImageRequest.Builder(context).apply {
crossfade(crossFadeDuration.inWholeMilliseconds.toInt())
val crossFadeDurationMs = crossFadeDuration.inWholeMilliseconds.toInt()
if (crossFadeDurationMs > 0) crossfade(crossFadeDurationMs)
else crossfade(false)
target(this@AsyncImageView)
data(url)
placeholder(placeholderOrBlurHash)

View File

@@ -34,7 +34,7 @@ import org.koin.compose.rememberKoinInject
private fun AppThemeBackground() {
val context = LocalContext.current
val themeBackground = remember(context.theme) {
val attributes = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
val attributes = context.theme.obtainStyledAttributes(intArrayOf(R.attr.defaultBackground))
val drawable = attributes.getDrawable(0)
attributes.recycle()
@@ -64,26 +64,29 @@ private fun AppThemeBackground() {
fun AppBackground() {
val backgroundService = rememberKoinInject<BackgroundService>()
val currentBackground by backgroundService.currentBackground.collectAsState()
val enabled by backgroundService.enabled.collectAsState()
AnimatedContent(
targetState = currentBackground,
transitionSpec = {
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
},
label = "BackgroundTransition",
) { background ->
if (background != null) {
Image(
bitmap = background,
contentDescription = null,
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
modifier = Modifier.fillMaxSize()
)
} else {
AppThemeBackground()
if (enabled) {
AnimatedContent(
targetState = currentBackground,
transitionSpec = {
val duration = (BackgroundService.TRANSITION_DURATION.inWholeMilliseconds / 2).toInt()
fadeIn(tween(durationMillis = duration)) togetherWith fadeOut(snap(delayMillis = duration))
},
label = "BackgroundTransition",
) { background ->
if (background != null) {
Image(
bitmap = background,
contentDescription = null,
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(colorResource(R.color.background_filter), BlendMode.SrcAtop),
modifier = Modifier.fillMaxSize()
)
} else {
AppThemeBackground()
}
}
}
}

View File

@@ -2,7 +2,6 @@ package org.jellyfin.androidtv.ui.browsing;
import static org.koin.java.KoinJavaComponent.inject;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
@@ -19,6 +18,7 @@ import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.leanback.widget.BaseGridView;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
@@ -85,7 +85,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
private final static int CHUNK_SIZE_MINIMUM = 25;
private String mainTitle;
private Activity mActivity;
private FragmentActivity mActivity;
private BaseRowItem mCurrentItem;
private CompositeClickedListener mClickedListener = new CompositeClickedListener();
private CompositeSelectedListener mSelectedListener = new CompositeSelectedListener();
@@ -758,7 +758,7 @@ public class BrowseGridFragment extends Fragment implements View.OnKeyListener {
libraryPreferences.set(LibraryPreferences.Companion.getFilterUnwatchedOnly(), mAdapter.getFilters().isUnwatchedOnly());
libraryPreferences.set(LibraryPreferences.Companion.getSortBy(), mAdapter.getSortBy());
libraryPreferences.set(LibraryPreferences.Companion.getSortOrder(), getSortOption(mAdapter.getSortBy()).order);
CoroutineUtils.runBlocking((coroutineScope, continuation) -> libraryPreferences.commit(continuation));
CoroutineUtils.runOnLifecycle(getLifecycle(), (coroutineScope, continuation) -> libraryPreferences.commit(continuation));
}
private void addTools() {

View File

@@ -478,7 +478,7 @@ public class BrowseViewFragment extends EnhancedBrowseFragment {
@Override
public void onError(Exception exception) {
exception.printStackTrace();
Timber.e(exception, "Failed to get items");
}
});
break;

View File

@@ -1,52 +1,52 @@
package org.jellyfin.androidtv.ui.browsing;
import android.os.Bundle;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.data.querying.StdItemQuery;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.apiclient.model.querying.ItemFields;
import org.jellyfin.sdk.model.api.BaseItemKind;
public class CollectionFragment extends EnhancedBrowseFragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
@Override
protected void setupQueries(RowLoader rowLoader) {
if (Utils.getSafeValue(mFolder.getChildCount(), 0) > 0) {
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
movies.setParentId(mFolder.getId().toString());
movies.setIncludeItemTypes(new String[]{"Movie"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_movies), movies, 100));
StdItemQuery movies = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
movies.setParentId(mFolder.getId().toString());
movies.setIncludeItemTypes(new String[]{BaseItemKind.MOVIE.getSerialName()});
mRows.add(new BrowseRowDef(getString(R.string.lbl_movies), movies, 100));
StdItemQuery series = new StdItemQuery();
series.setParentId(mFolder.getId().toString());
series.setIncludeItemTypes(new String[]{"Series"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_tv_series), series, 100));
StdItemQuery others = new StdItemQuery();
others.setParentId(mFolder.getId().toString());
others.setExcludeItemTypes(new String[]{"Movie", "Series"});
mRows.add(new BrowseRowDef(getString(R.string.lbl_other), others, 100));
rowLoader.loadRows(mRows);
}
StdItemQuery series = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
series.setParentId(mFolder.getId().toString());
series.setIncludeItemTypes(new String[]{BaseItemKind.SERIES.getSerialName()});
mRows.add(new BrowseRowDef(getString(R.string.lbl_tv_series), series, 100));
StdItemQuery others = new StdItemQuery(new ItemFields[]{
ItemFields.PrimaryImageAspectRatio,
ItemFields.Overview,
ItemFields.ItemCounts,
ItemFields.DisplayPreferencesId,
ItemFields.ChildCount,
ItemFields.MediaStreams,
ItemFields.MediaSources
});
others.setParentId(mFolder.getId().toString());
others.setExcludeItemTypes(new String[]{BaseItemKind.MOVIE.getSerialName(), BaseItemKind.SERIES.getSerialName()});
mRows.add(new BrowseRowDef(getString(R.string.lbl_other), others, 100));
rowLoader.loadRows(mRows);
}
}

View File

@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.ui.browsing
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
@@ -26,6 +27,7 @@ import org.jellyfin.androidtv.ui.navigation.NavigationRepository
import org.jellyfin.androidtv.ui.screensaver.InAppScreensaver
import org.jellyfin.androidtv.ui.startup.StartupActivity
import org.jellyfin.androidtv.util.applyTheme
import org.jellyfin.androidtv.util.isMediaSessionKeyEvent
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
@@ -193,4 +195,38 @@ class MainActivity : FragmentActivity() {
screensaverViewModel.notifyInteraction(false)
}
@Suppress("RestrictedApi") // False positive
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Ignore the key event that closes the screensaver
if (!event.isMediaSessionKeyEvent() && screensaverViewModel.visible.value) {
screensaverViewModel.notifyInteraction(canCancel = event.action == KeyEvent.ACTION_UP)
return true
}
@Suppress("RestrictedApi") // False positive
return super.dispatchKeyEvent(event)
}
@Suppress("RestrictedApi") // False positive
override fun dispatchKeyShortcutEvent(event: KeyEvent): Boolean {
// Ignore the key event that closes the screensaver
if (!event.isMediaSessionKeyEvent() && screensaverViewModel.visible.value) {
screensaverViewModel.notifyInteraction(canCancel = event.action == KeyEvent.ACTION_UP)
return true
}
@Suppress("RestrictedApi") // False positive
return super.dispatchKeyShortcutEvent(event)
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
// Ignore the touch event that closes the screensaver
if (screensaverViewModel.visible.value) {
screensaverViewModel.notifyInteraction(true)
return true
}
return super.dispatchTouchEvent(ev)
}
}

View File

@@ -11,10 +11,13 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.auth.repository.ServerRepository
import org.jellyfin.androidtv.auth.repository.SessionRepository
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.data.repository.NotificationsRepository
import org.jellyfin.androidtv.databinding.FragmentHomeBinding
import org.jellyfin.androidtv.ui.navigation.Destinations
import org.jellyfin.androidtv.ui.navigation.NavigationRepository
@@ -29,6 +32,8 @@ class HomeFragment : Fragment() {
private val sessionRepository by inject<SessionRepository>()
private val userRepository by inject<UserRepository>()
private val serverRepository by inject<ServerRepository>()
private val notificationRepository by inject<NotificationsRepository>()
private val navigationRepository by inject<NavigationRepository>()
private val mediaManager by inject<MediaManager>()
@@ -44,7 +49,7 @@ class HomeFragment : Fragment() {
}
binding.search.setOnClickListener {
navigationRepository.navigate(Destinations.search)
navigationRepository.navigate(Destinations.search())
}
return binding.root
@@ -62,7 +67,19 @@ class HomeFragment : Fragment() {
placeholder = ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)
)
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
sessionRepository.currentSession
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.map { session->
if (session == null) null
else serverRepository.getServer(session.serverId)
}
.onEach { server ->
notificationRepository.updateServerNotifications(server)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
override fun onDestroyView() {

View File

@@ -17,11 +17,14 @@ import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.jellyfin.androidtv.auth.repository.UserRepository
import org.jellyfin.androidtv.constant.CustomMessage
import org.jellyfin.androidtv.constant.HomeSectionType
@@ -88,15 +91,17 @@ class HomeRowsFragment : RowsSupportFragment(), AudioEventListener, View.OnKeyLi
adapter = MutableObjectAdapter<Row>(PositionableListRowPresenter())
val currentUser = userRepository.currentUser.value
lifecycleScope.launch(Dispatchers.IO) {
val currentUser = withTimeout(30.seconds) {
userRepository.currentUser.filterNotNull().first()
}
// Start out with default sections
val homesections = userSettingPreferences.homesections
var includeLiveTvRows = false
// Check for live TV support
if (homesections.contains(HomeSectionType.LIVE_TV) && currentUser?.policy?.enableLiveTvAccess == true) {
if (homesections.contains(HomeSectionType.LIVE_TV) && currentUser.policy?.enableLiveTvAccess == true) {
// This is kind of ugly, but it mirrors how web handles the live TV rows on the home screen
// If we can retrieve one live TV recommendation, then we should display the rows
val recommendedPrograms by api.liveTvApi.getRecommendedPrograms(

View File

@@ -334,9 +334,13 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
@Override
public void run() {
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return;
// View holder may be null when the base item is still loading - this is a rare case
// which generally happens when the server is unresponsive
MyDetailsOverviewRowPresenter.ViewHolder viewholder = mDorPresenter.getViewHolder();
if (viewholder == null) return;
if (mBaseItem != null && ((mBaseItem.getRunTimeTicks() != null && mBaseItem.getRunTimeTicks() > 0) || mBaseItem.getOriginalRunTimeTicks() != null)) {
mDorPresenter.getViewHolder().setInfoValue3(getEndTime());
viewholder.setInfoValue3(getEndTime());
mLoopHandler.postDelayed(this, 15000);
}
}
@@ -412,6 +416,14 @@ public class FullDetailsFragment extends Fragment implements RecordingIndicatorV
setBaseItem(response);
}
@Override
public void onError(@Nullable Exception exception) {
Timber.w(exception, "Failed to load item, trying to navigate back.");
super.onError(exception);
navigationRepository.getValue().goBack();
}
});
}

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import androidx.annotation.Nullable;
import org.jellyfin.androidtv.constant.LiveTvOption;
import org.jellyfin.androidtv.constant.QueryType;
import org.jellyfin.androidtv.data.model.ChapterItemInfo;
import org.jellyfin.androidtv.preference.LibraryPreferences;
import org.jellyfin.androidtv.preference.PreferencesRepository;
@@ -106,6 +107,8 @@ public class ItemLauncher {
} else if (mediaManager.hasAudioQueueItems() && rowItem instanceof AudioQueueItem && pos < mediaManager.getCurrentAudioQueueSize()) {
Timber.d("playing audio queue item");
mediaManager.playFrom(pos);
} else if (adapter.getQueryType() == QueryType.Search) {
mediaManager.playNow(context, rowItem.getBaseItem());
} else {
Timber.d("playing audio item");
List<BaseItemDto> audioItemsAsList = new ArrayList<>();
@@ -138,7 +141,7 @@ public class ItemLauncher {
}
// or generic handling
if (baseItem.isFolder()) {
if (Utils.isTrue(baseItem.isFolder())) {
// Some items don't have a display preferences id, but it's required for StdGridFragment
// Use the id of the item as a workaround, it's a unique key for the specific item
// Which is exactly what we want

View File

@@ -87,6 +87,7 @@ public class TvManager {
public static boolean shouldForceReload() { return forceReload; }
public static int getAllChannelsIndex(String id) {
if (allChannels == null) return -1;
for (int i = 0; i < allChannels.size(); i++) {
if (allChannels.get(i).getId().equals(id)) return i;
}

View File

@@ -46,7 +46,9 @@ object Destinations {
// General
val home = fragmentDestination<HomeFragment>()
val search = fragmentDestination<SearchFragment>()
fun search(query: String? = null) = fragmentDestination<SearchFragment>(
SearchFragment.EXTRA_QUERY to query,
)
val userPreferences = preferenceDestination<UserPreferencesScreen>()
// Browsing

View File

@@ -203,7 +203,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
}
};
backgroundService.getValue().clearBackgrounds();
backgroundService.getValue().disable();
}
@Nullable

View File

@@ -9,14 +9,16 @@ import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSourceFactory;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.constant.QueryType;
@@ -53,6 +55,7 @@ import java.util.Random;
import kotlin.Lazy;
import timber.log.Timber;
@OptIn(markerClass = UnstableApi.class)
public class LegacyMediaManager implements MediaManager {
private Context context;

View File

@@ -35,6 +35,7 @@ import org.jellyfin.androidtv.util.profile.ExoPlayerProfile;
import org.jellyfin.androidtv.util.profile.LibVlcProfile;
import org.jellyfin.androidtv.util.sdk.ModelUtils;
import org.jellyfin.androidtv.util.sdk.compat.JavaCompat;
import org.jellyfin.androidtv.util.sdk.compat.ModelCompat;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dlna.DeviceProfile;
@@ -51,6 +52,8 @@ import org.jellyfin.sdk.model.api.MediaStreamType;
import org.jellyfin.sdk.model.api.PlayAccess;
import org.koin.java.KoinJavaComponent;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import kotlin.Lazy;
@@ -101,8 +104,8 @@ public class PlaybackController implements PlaybackControllerNotifiable {
private boolean wasSeeking = false;
private boolean finishedInitialSeek = false;
private long mCurrentProgramEndTime;
private long mCurrentProgramStartTime = 0;
private LocalDateTime mCurrentProgramEnd = null;
private LocalDateTime mCurrentProgramStart = null;
private long mCurrentTranscodeStartTime;
private boolean isLiveTv = false;
private boolean directStreamLiveTv;
@@ -404,7 +407,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
private void refreshCurrentPosition() {
long newPos = -1;
if (isLiveTv && mCurrentProgramStartTime > 0) {
if (isLiveTv && mCurrentProgramStart != null) {
newPos = getRealTimeProgress();
// live tv
} else if (hasInitializedVideoManager()) {
@@ -436,7 +439,12 @@ public class PlaybackController implements PlaybackControllerNotifiable {
}
private void play(long position, @Nullable Integer forcedSubtitleIndex) {
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);
Timber.i("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);
if (mFragment == null) {
Timber.w("mFragment is null, returning");
return;
}
if (position < 0) {
Timber.i("Negative start requested - adjusting to zero");
@@ -455,8 +463,7 @@ public class PlaybackController implements PlaybackControllerNotifiable {
mVideoManager.play();
if (mVideoManager.isNativeMode())
mPlaybackState = PlaybackState.PLAYING; //won't get another onprepared call
if (mFragment != null)
mFragment.setFadingEnabled(true);
mFragment.setFadingEnabled(true);
startReportLoop();
break;
case BUFFERING:
@@ -469,23 +476,19 @@ public class PlaybackController implements PlaybackControllerNotifiable {
mSeekPosition = position;
mCurrentPosition = 0;
if (mFragment != null) {
mFragment.setFadingEnabled(false);
}
mFragment.setFadingEnabled(false);
org.jellyfin.sdk.model.api.BaseItemDto item = getCurrentlyPlayingItem();
if (item == null) {
Timber.d("item is null - aborting play");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_cannot_play));
mFragment.closePlayer();
}
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_cannot_play));
mFragment.closePlayer();
return;
}
// make sure item isn't missing
if (item.getLocationType() == LocationType.VIRTUAL && mFragment != null) {
if (item.getLocationType() == LocationType.VIRTUAL) {
if (hasNextItem()) {
new AlertDialog.Builder(mFragment.getContext())
.setTitle(R.string.episode_missing)
@@ -522,8 +525,6 @@ public class PlaybackController implements PlaybackControllerNotifiable {
// confirm we actually can play
if (item.getPlayAccess() != PlayAccess.FULL) {
if (mFragment == null) return;
String msg = item.isPlaceHolder() ? mFragment.getString(R.string.msg_cannot_play) : mFragment.getString(R.string.msg_cannot_play_time);
Utils.showToast(mFragment.getContext(), msg);
return;
@@ -542,10 +543,8 @@ public class PlaybackController implements PlaybackControllerNotifiable {
playInternal(getCurrentlyPlayingItem(), position, vlcOptions, internalOptions);
mPlaybackState = PlaybackState.BUFFERING;
if (mFragment != null) {
mFragment.setPlayPauseActionState(0);
mFragment.setCurrentTime(position);
}
mFragment.setPlayPauseActionState(0);
mFragment.setCurrentTime(position);
long duration = getCurrentlyPlayingItem().getRunTimeTicks() != null ? getCurrentlyPlayingItem().getRunTimeTicks() / 10000 : -1;
if (mVideoManager != null)
@@ -1293,8 +1292,9 @@ public class PlaybackController implements PlaybackControllerNotifiable {
apiClient.getValue().GetLiveTvChannelAsync(channel.getId().toString(), KoinJavaComponent.<UserRepository>get(UserRepository.class).getCurrentUser().getValue().getId().toString(), new Response<ChannelInfoDto>() {
@Override
public void onResponse(ChannelInfoDto response) {
BaseItemDto program = response.getCurrentProgram();
if (program != null) {
BaseItemDto legacyProgram = response.getCurrentProgram();
if (legacyProgram != null) {
org.jellyfin.sdk.model.api.BaseItemDto program = ModelCompat.asSdk(legacyProgram);
// TODO Do we need these setters?
// channel.setName(program.getName() + liveTvChannelName);
// channel.setPremiereDate(program.getStartDate());
@@ -1303,8 +1303,8 @@ public class PlaybackController implements PlaybackControllerNotifiable {
// channel.setOverview(program.getOverview());
// channel.setRunTimeTicks(program.getRunTimeTicks());
// channel.setCurrentProgram(program);
mCurrentProgramEndTime = program.getEndDate() != null ? program.getEndDate().getTime() : 0;
mCurrentProgramStartTime = program.getPremiereDate() != null ? program.getPremiereDate().getTime() : 0;
mCurrentProgramEnd = program.getEndDate();
mCurrentProgramStart = program.getPremiereDate();
if (mFragment != null) mFragment.updateDisplay();
}
}
@@ -1314,12 +1314,16 @@ public class PlaybackController implements PlaybackControllerNotifiable {
}
private long getRealTimeProgress() {
return System.currentTimeMillis() - mCurrentProgramStartTime;
long progress = System.currentTimeMillis();
if (mCurrentProgramStart != null) {
progress -= mCurrentProgramStart.toInstant(ZoneOffset.UTC).toEpochMilli();
}
return progress;
}
private long getTimeShiftedProgress() {
refreshCurrentPosition();
return !directStreamLiveTv ? mCurrentPosition + (mCurrentTranscodeStartTime - mCurrentProgramStartTime) : getRealTimeProgress();
return !directStreamLiveTv ? mCurrentPosition + (mCurrentTranscodeStartTime - (mCurrentProgramStart == null ? 0 : mCurrentProgramStart.toInstant(ZoneOffset.UTC).toEpochMilli())) : getRealTimeProgress();
}
private void startReportLoop() {
@@ -1506,10 +1510,6 @@ public class PlaybackController implements PlaybackControllerNotifiable {
}
}
if (isLiveTv && mCurrentProgramEndTime > 0 && System.currentTimeMillis() >= mCurrentProgramEndTime) {
// crossed fire off an async routine to update the program info
updateTvProgramInfo();
}
if (mFragment != null && finishedInitialSeek)
mFragment.updateSubtitles(mCurrentPosition - getSubtitleDelay());
}

View File

@@ -18,25 +18,28 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.ts.TsExtractor;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.common.collect.ImmutableSet;
import org.jellyfin.androidtv.R;
@@ -58,6 +61,7 @@ import java.util.Optional;
import timber.log.Timber;
@OptIn(markerClass = UnstableApi.class)
public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public final static int ZOOM_FIT = 0;
public final static int ZOOM_AUTO_CROP = 1;
@@ -76,7 +80,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private SurfaceView mSubtitlesSurface;
private FrameLayout mSurfaceFrame;
private ExoPlayer mExoPlayer;
private StyledPlayerView mExoPlayerView;
private PlayerView mExoPlayerView;
private LibVLC mLibVLC;
private MediaPlayer mVlcPlayer;
private Media mCurrentMedia;
@@ -94,7 +98,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private long mMetaDuration = -1;
private long mMetaVLCStreamStartPosition = -1;
private long lastExoPlayerPosition = -1;
private boolean nightModeEnabled = false;
private boolean nightModeEnabled;
private boolean nativeMode = false;
private boolean mSurfaceReady = false;
@@ -202,6 +206,16 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
defaultRendererFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
exoPlayerBuilder.setRenderersFactory(defaultRendererFactory);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(trackSelector.buildUponParameters()
.setAudioOffloadPreferences(new TrackSelectionParameters.AudioOffloadPreferences.Builder()
.setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
.build()
)
.build()
);
exoPlayerBuilder.setTrackSelector(trackSelector);
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory().setTsExtractorTimestampSearchBytes(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES * 3);
exoPlayerBuilder.setMediaSourceFactory(new DefaultMediaSourceFactory(context, defaultExtractorsFactory));

View File

@@ -31,6 +31,9 @@ class LeanbackSearchFragment : SearchSupportFragment(), SearchSupportFragment.Se
viewModel.searchResultsFlow
.onEach { searchFragmentDelegate.showResults(it) }
.launchIn(lifecycleScope)
val query = arguments?.getString(SearchFragment.EXTRA_QUERY)
if (!query.isNullOrBlank()) setSearchQuery(query, true)
}
override fun getResultsAdapter() = searchFragmentDelegate.rowsAdapter

View File

@@ -6,9 +6,14 @@ import android.os.Bundle
import android.speech.SpeechRecognizer
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import org.jellyfin.androidtv.R
class SearchFragment : Fragment(R.layout.fragment_content_view) {
companion object {
const val EXTRA_QUERY = "query"
}
private val isSpeechEnabled by lazy {
SpeechRecognizer.isRecognitionAvailable(requireContext())
&& ContextCompat.checkSelfPermission(
@@ -22,14 +27,13 @@ class SearchFragment : Fragment(R.layout.fragment_content_view) {
// Determine fragment to use
val searchFragment = when {
isSpeechEnabled -> LeanbackSearchFragment()
else -> TextSearchFragment()
isSpeechEnabled -> LeanbackSearchFragment::class.java
else -> TextSearchFragment::class.java
}
// Add fragment
childFragmentManager
.beginTransaction()
.replace(R.id.content_view, searchFragment)
.commit()
childFragmentManager.commit {
replace(R.id.content_view, searchFragment, arguments)
}
}
}

View File

@@ -7,8 +7,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.leanback.app.RowsSupportFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
@@ -34,26 +37,40 @@ class TextSearchFragment : Fragment() {
savedInstanceState: Bundle?
): View {
_binding = FragmentSearchTextBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.searchBar.apply {
onTextChanged { viewModel.searchDebounced(it) }
onSubmit { viewModel.searchImmediately(it) }
}
binding.resultsFrame.getFragment<RowsSupportFragment?>()?.let {
it.adapter = searchFragmentDelegate.rowsAdapter
it.onItemViewClickedListener = searchFragmentDelegate.onItemViewClickedListener
it.onItemViewSelectedListener = searchFragmentDelegate.onItemViewSelectedListener
val rowsSupportFragment = RowsSupportFragment().apply {
adapter = searchFragmentDelegate.rowsAdapter
onItemViewClickedListener = searchFragmentDelegate.onItemViewClickedListener
onItemViewSelectedListener = searchFragmentDelegate.onItemViewSelectedListener
}
childFragmentManager.commit {
replace(binding.resultsFrame.id, rowsSupportFragment)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.searchResultsFlow
.onEach { searchFragmentDelegate.showResults(it) }
.launchIn(lifecycleScope)
val query = arguments?.getString(SearchFragment.EXTRA_QUERY)
if (!query.isNullOrBlank()) {
binding.searchBar.setText(query)
viewModel.searchImmediately(query)
binding.resultsFrame.requestFocus()
} else {
binding.searchBar.requestFocus()
}
}
override fun onDestroyView() {
@@ -63,12 +80,23 @@ class TextSearchFragment : Fragment() {
}
private fun EditText.onSubmit(onSubmit: (String) -> Unit) {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
onSubmit(text.toString())
true
} else {
false
setOnEditorActionListener { view, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE,
EditorInfo.IME_ACTION_SEARCH,
EditorInfo.IME_ACTION_PREVIOUS -> {
onSubmit(text.toString())
// Manually close IME to workaround focus issue with Fire TV
context.getSystemService<InputMethodManager>()
?.hideSoftInputFromWindow(view.windowToken, 0)
// Focus on search results
binding.resultsFrame.requestFocus()
true
}
else -> false
}
}
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.ui.startup
import android.Manifest
import android.app.SearchManager
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
@@ -137,7 +138,9 @@ class StartupActivity : FragmentActivity() {
// Create destination
val destination = when {
// Search is requested
intent.action === Intent.ACTION_SEARCH -> Destinations.search
intent.action === Intent.ACTION_SEARCH -> Destinations.search(
query = intent.getStringExtra(SearchManager.QUERY)
)
// User view item is requested
itemId != null && itemIsUserView -> {
val item by api.userLibraryApi.getItem(itemId = itemId)
@@ -180,4 +183,9 @@ class StartupActivity : FragmentActivity() {
replace<StartupToolbarFragment>(R.id.content_view)
add<SelectServerFragment>(R.id.content_view)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
}

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
@@ -33,7 +34,9 @@ fun SplashScreen() {
Image(
painter = painterResource(R.drawable.app_logo),
contentDescription = stringResource(R.string.app_name),
modifier = Modifier.width(400.dp)
modifier = Modifier
.width(400.dp)
.fillMaxHeight()
)
}
}

View File

@@ -8,12 +8,14 @@ import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.constant.CustomMessage
import org.jellyfin.androidtv.data.repository.CustomMessageRepository
fun <T : Any> runBlocking(block: suspend CoroutineScope.() -> T) = kotlinx.coroutines.runBlocking {
block()
}
fun <T : Any> runOnLifecycle(
lifecycle: Lifecycle,
block: suspend CoroutineScope.() -> T
) = lifecycle.coroutineScope.launch { block() }
fun readCustomMessagesOnLifecycle(
lifecycle: Lifecycle,

View File

@@ -107,7 +107,7 @@ public class InfoLayoutHelper {
if (includeRuntime) addRuntime(context, item, layout, includeEndTime);
addSeriesStatus(context, item, layout);
addRatingAndRes(context, item, mediaSourceIndex, layout);
addMediaDetails(context, audioStream, layout);
addMediaDetails(context, item, mediaSourceIndex, layout);
}
private static void addText(Context context, String text, LinearLayout layout, int maxWidth) {
@@ -392,25 +392,27 @@ public class InfoLayoutHelper {
}
}
private static void addMediaDetails(Context context, MediaStream stream, LinearLayout layout) {
private static void addMediaDetails(Context context, BaseItemDto item, int mediaSourceIndex, LinearLayout layout) {
if (stream != null) {
if (stream.getProfile() != null && stream.getProfile().contains("Dolby Atmos")) {
MediaStream audioStream = StreamHelper.getFirstAudioStream(item, mediaSourceIndex);
if (audioStream != null) {
if (audioStream.getProfile() != null && audioStream.getProfile().contains("Dolby Atmos")) {
addBlockText(context, layout, "ATMOS");
addSpacer(context, layout, " ");
} else if (stream.getProfile() != null && stream.getProfile().contains("DTS:X")) {
} else if (audioStream.getProfile() != null && audioStream.getProfile().contains("DTS:X")) {
addBlockText(context, layout, "DTS:X");
addSpacer(context, layout, " ");
} else {
String codec = null;
if (stream.getProfile() != null && stream.getProfile().contains("DTS-HD")) {
if (audioStream.getProfile() != null && audioStream.getProfile().contains("DTS-HD")) {
codec = "DTS-HD";
} else if (stream.getCodec() != null && stream.getCodec().trim().length() > 0) {
switch (stream.getCodec().toLowerCase()) {
} else if (audioStream.getCodec() != null &audioStream.getCodec().trim().length() > 0) {
switch (audioStream.getCodec().toLowerCase()) {
case "dca": codec = "DTS"; break;
case "eac3": codec = "DD+"; break;
case "ac3": codec = "DD"; break;
default: codec = stream.getCodec().toUpperCase();
default: codec = audioStream.getCodec().toUpperCase();
}
}
if (codec != null) {
@@ -418,8 +420,8 @@ public class InfoLayoutHelper {
addSpacer(context, layout, " ");
}
}
if (stream.getChannelLayout() != null && stream.getChannelLayout().trim().length() > 0) {
addBlockText(context, layout, stream.getChannelLayout().toUpperCase());
if (audioStream.getChannelLayout() != null && audioStream.getChannelLayout().trim().length() > 0) {
addBlockText(context, layout, audioStream.getChannelLayout().toUpperCase());
addSpacer(context, layout, " ");
}
}

View File

@@ -0,0 +1,26 @@
package org.jellyfin.androidtv.util
import android.os.Build
import android.view.KeyEvent
/**
* Returns whether this key event is a media key event or not.
*/
fun KeyEvent.isMediaSessionKeyEvent(): Boolean = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> KeyEvent.isMediaSessionKey(keyCode)
else -> when (keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_STOP,
KeyEvent.KEYCODE_MEDIA_NEXT,
KeyEvent.KEYCODE_MEDIA_PREVIOUS,
KeyEvent.KEYCODE_MEDIA_REWIND,
KeyEvent.KEYCODE_MEDIA_RECORD,
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> true
else -> false
}
}

View File

@@ -1,11 +1,13 @@
package org.jellyfin.androidtv.util;
import android.app.Activity;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.widget.PopupMenu;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.constant.CustomMessage;
import org.jellyfin.androidtv.data.querying.StdItemQuery;
@@ -55,11 +57,11 @@ public class KeyProcessor {
private static String mCurrentItemId;
private static BaseItemDto mCurrentItem;
private static Activity mCurrentActivity;
private static FragmentActivity mCurrentActivity;
private static int mCurrentRowItemNdx;
private static boolean isMusic;
public static boolean HandleKey(int key, BaseRowItem rowItem, Activity activity) {
public static boolean HandleKey(int key, BaseRowItem rowItem, FragmentActivity activity) {
if (rowItem == null) return false;
MediaManager mediaManager = KoinJavaComponent.<MediaManager>get(MediaManager.class);
switch (key) {
@@ -188,7 +190,7 @@ public class KeyProcessor {
return false;
}
public static PopupMenu createItemMenu(BaseRowItem rowItem, UserItemDataDto userData, Activity activity) {
public static PopupMenu createItemMenu(BaseRowItem rowItem, UserItemDataDto userData, FragmentActivity activity) {
BaseItemDto item = rowItem.getBaseItem();
PopupMenu menu = new PopupMenu(activity, activity.getCurrentFocus(), Gravity.END);
int order = 0;
@@ -278,7 +280,7 @@ public class KeyProcessor {
return menu;
}
private static void createPlayMenu(BaseItemDto item, boolean isMusic, Activity activity) {
private static void createPlayMenu(BaseItemDto item, boolean isMusic, FragmentActivity activity) {
PopupMenu menu = new PopupMenu(activity, activity.getCurrentFocus(), Gravity.END);
int order = 0;
if (!isMusic && item.getType() != BaseItemKind.PLAYLIST) {
@@ -360,16 +362,16 @@ public class KeyProcessor {
});
return true;
case MENU_MARK_FAVORITE:
toggleFavorite(true);
toggleFavorite(mCurrentActivity, true);
return true;
case MENU_UNMARK_FAVORITE:
toggleFavorite(false);
toggleFavorite(mCurrentActivity, false);
return true;
case MENU_MARK_PLAYED:
togglePlayed(true);
togglePlayed(mCurrentActivity, true);
return true;
case MENU_UNMARK_PLAYED:
togglePlayed(false);
togglePlayed(mCurrentActivity, false);
return true;
case MENU_GOTO_NOW_PLAYING:
NavigationRepository navigationRepository = KoinJavaComponent.get(NavigationRepository.class);
@@ -396,10 +398,10 @@ public class KeyProcessor {
}
};
private static void togglePlayed(boolean played) {
private static void togglePlayed(LifecycleOwner lifecycleOwner, boolean played) {
ItemMutationRepository itemMutationRepository = KoinJavaComponent.<ItemMutationRepository>get(ItemMutationRepository.class);
CoroutineUtils.runBlocking((scope, continuation) ->
CoroutineUtils.runOnLifecycle(lifecycleOwner.getLifecycle(), (scope, continuation) ->
itemMutationRepository.setPlayed(mCurrentItem.getId(), played, continuation)
);
@@ -407,10 +409,10 @@ public class KeyProcessor {
customMessageRepository.pushMessage(CustomMessage.RefreshCurrentItem.INSTANCE);
}
private static void toggleFavorite(boolean favorite) {
private static void toggleFavorite(LifecycleOwner lifecycleOwner, boolean favorite) {
ItemMutationRepository itemMutationRepository = KoinJavaComponent.<ItemMutationRepository>get(ItemMutationRepository.class);
CoroutineUtils.runBlocking((scope, continuation) ->
CoroutineUtils.runOnLifecycle(lifecycleOwner.getLifecycle(), (scope, continuation) ->
itemMutationRepository.setFavorite(mCurrentItem.getId(), favorite, continuation)
);

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.util.apiclient;
import android.content.Context;
import android.provider.MediaStore;
import org.jellyfin.androidtv.R;
import org.jellyfin.androidtv.auth.repository.SessionRepository;
@@ -8,6 +9,8 @@ import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.ui.navigation.Destination;
import org.jellyfin.androidtv.ui.navigation.NavigationRepository;
import org.jellyfin.androidtv.ui.playback.MediaManager;
import org.jellyfin.androidtv.ui.playback.PlaybackController;
import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer;
import org.jellyfin.androidtv.ui.playback.PlaybackLauncher;
import org.jellyfin.androidtv.ui.playback.VideoQueueManager;
import org.jellyfin.androidtv.util.TimeUtils;
@@ -298,7 +301,12 @@ public class PlaybackHelper {
default:
KoinJavaComponent.<VideoQueueManager>get(VideoQueueManager.class).setCurrentVideoQueue(response);
Destination destination = playbackLauncher.getPlaybackDestination(item.getType(), pos);
navigationRepository.navigate(destination);
PlaybackController playbackController = KoinJavaComponent.<PlaybackControllerContainer>get(PlaybackControllerContainer.class).getPlaybackController();
navigationRepository.navigate(
destination,
playbackController != null && playbackController.hasFragment()
);
}
}
});

View File

@@ -7,6 +7,7 @@ import org.jellyfin.androidtv.util.Utils
import org.jellyfin.androidtv.util.profile.ProfileHelper.audioDirectPlayProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceAV1CodecProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceHevcCodecProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceHevcLevelCodecProfiles
import org.jellyfin.androidtv.util.profile.ProfileHelper.h264VideoLevelProfileCondition
import org.jellyfin.androidtv.util.profile.ProfileHelper.h264VideoProfileCondition
import org.jellyfin.androidtv.util.profile.ProfileHelper.max1080pProfileConditions
@@ -191,8 +192,9 @@ class ExoPlayerProfile(
)
)
})
// HEVC profile
// HEVC profiles
add(deviceHevcCodecProfile)
addAll(deviceHevcLevelCodecProfiles)
// AV1 profile
add(deviceAV1CodecProfile)
// Limit video resolution support for older devices
@@ -203,7 +205,8 @@ class ExoPlayerProfile(
})
}
// Audio channel profile
add(maxAudioChannelsCodecProfile(channels = 8))
if (!Utils.downMixAudio(context)) add(maxAudioChannelsCodecProfile(channels = 8))
else add(maxAudioChannelsCodecProfile(channels = 2))
}.toTypedArray()
subtitleProfiles = arrayOf(

View File

@@ -5,6 +5,7 @@ import org.jellyfin.androidtv.constant.Codec
import org.jellyfin.androidtv.util.Utils
import org.jellyfin.androidtv.util.profile.ProfileHelper.audioDirectPlayProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceHevcCodecProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceHevcLevelCodecProfiles
import org.jellyfin.androidtv.util.profile.ProfileHelper.h264VideoLevelProfileCondition
import org.jellyfin.androidtv.util.profile.ProfileHelper.h264VideoProfileCondition
import org.jellyfin.androidtv.util.profile.ProfileHelper.maxAudioChannelsCodecProfile
@@ -116,21 +117,22 @@ class LibVlcProfile(
photoDirectPlayProfile
)
codecProfiles = arrayOf(
codecProfiles = buildList {
// HEVC profile
deviceHevcCodecProfile,
add(deviceHevcCodecProfile)
addAll(deviceHevcLevelCodecProfiles)
// H264 profile
CodecProfile().apply {
add(CodecProfile().apply {
type = CodecType.Video
codec = Codec.Video.H264
conditions = arrayOf(
h264VideoProfileCondition,
h264VideoLevelProfileCondition
)
},
})
// Audio channel profile
maxAudioChannelsCodecProfile(channels = 8)
)
add(maxAudioChannelsCodecProfile(channels = 8))
}.toTypedArray()
containerProfiles = arrayOf(
ContainerProfile().apply {

View File

@@ -9,6 +9,23 @@ import timber.log.Timber
class MediaCodecCapabilitiesTest {
private val mediaCodecList by lazy { MediaCodecList(MediaCodecList.REGULAR_CODECS) }
// HEVC levels as reported by ffprobe are multiplied by 30, e.g. level 4.1 is 123
private val hevcLevelStrings = listOf(
CodecProfileLevel.HEVCMainTierLevel1 to "30",
CodecProfileLevel.HEVCMainTierLevel2 to "60",
CodecProfileLevel.HEVCMainTierLevel21 to "63",
CodecProfileLevel.HEVCMainTierLevel3 to "90",
CodecProfileLevel.HEVCMainTierLevel31 to "93",
CodecProfileLevel.HEVCMainTierLevel4 to "120",
CodecProfileLevel.HEVCMainTierLevel41 to "123",
CodecProfileLevel.HEVCMainTierLevel5 to "150",
CodecProfileLevel.HEVCMainTierLevel51 to "153",
CodecProfileLevel.HEVCMainTierLevel52 to "156",
CodecProfileLevel.HEVCMainTierLevel6 to "180",
CodecProfileLevel.HEVCMainTierLevel61 to "183",
CodecProfileLevel.HEVCMainTierLevel62 to "186",
)
fun supportsAV1(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
hasCodecForMime(MediaFormat.MIMETYPE_VIDEO_AV1)
@@ -24,7 +41,15 @@ class MediaCodecCapabilitiesTest {
fun supportsHevcMain10(): Boolean = hasDecoder(
MediaFormat.MIMETYPE_VIDEO_HEVC,
CodecProfileLevel.HEVCProfileMain10,
CodecProfileLevel.HEVCMainTierLevel5
CodecProfileLevel.HEVCMainTierLevel4
)
fun getHevcMainLevel(): String = getHevcLevelString(
CodecProfileLevel.HEVCProfileMain
)
fun getHevcMain10Level(): String = getHevcLevelString(
CodecProfileLevel.HEVCProfileMain10
)
fun supportsAVCHigh10(): Boolean = hasDecoder(
@@ -33,6 +58,35 @@ class MediaCodecCapabilitiesTest {
CodecProfileLevel.AVCLevel4
)
private fun getHevcLevelString(profile: Int): String {
val level = getDecoderLevel(MediaFormat.MIMETYPE_VIDEO_HEVC, profile)
return hevcLevelStrings.asReversed().find { item: Pair<Int, String> ->
level >= item.first
}?.second ?: "0"
}
private fun getDecoderLevel(mime: String, profile: Int): Int {
var maxLevel = 0
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue
try {
val capabilities = info.getCapabilitiesForType(mime)
for (profileLevel in capabilities.profileLevels) {
if (profileLevel.profile == profile) {
maxLevel = maxOf(maxLevel, profileLevel.level)
}
}
} catch (e: IllegalArgumentException) {
Timber.d(e, "Decoder %s does not support %s", info.name, mime)
}
}
return maxLevel
}
private fun hasDecoder(mime: String, profile: Int, level: Int): Boolean {
for (info in mediaCodecList.codecInfos) {
if (info.isEncoder) continue

View File

@@ -105,6 +105,56 @@ object ProfileHelper {
}
}
val deviceHevcLevelCodecProfiles by lazy {
buildList {
if (MediaTest.supportsHevc()) {
add(CodecProfile().apply {
type = CodecType.Video
codec = Codec.Video.HEVC
applyConditions = arrayOf(
ProfileCondition(
ProfileConditionType.Equals,
ProfileConditionValue.VideoProfile,
"Main"
)
)
conditions = arrayOf(
ProfileCondition(
ProfileConditionType.LessThanEqual,
ProfileConditionValue.VideoLevel,
MediaTest.getHevcMainLevel()
)
)
})
if (MediaTest.supportsHevcMain10()) {
add(CodecProfile().apply {
type = CodecType.Video
codec = Codec.Video.HEVC
applyConditions = arrayOf(
ProfileCondition(
ProfileConditionType.Equals,
ProfileConditionValue.VideoProfile,
"Main 10"
)
)
conditions = arrayOf(
ProfileCondition(
ProfileConditionType.LessThanEqual,
ProfileConditionValue.VideoLevel,
MediaTest.getHevcMain10Level()
)
)
})
}
}
}
}
val h264VideoLevelProfileCondition by lazy {
ProfileCondition(
ProfileConditionType.LessThanEqual,

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<path
android:fillColor="@color/logo_background"
android:pathData="M0,0h320v180h-320z" />
</vector>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<path android:pathData="m63.761,45.877c-11.678,0 -49.27,68.152 -43.538,79.661s81.407,11.377 87.076,0 -31.862,-79.661 -43.538,-79.661zM63.766,63.355c7.651,0 32.241,44.737 28.527,52.196 -3.714,7.457 -53.296,7.541 -57.054,0s20.876,-52.196 28.527,-52.196zM63.761,79.688c-3.876,0 -16.352,22.619 -14.448,26.437s27.016,3.777 28.897,0 -10.572,-26.435 -14.448,-26.437z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="108.26714"
android:endY="124.14592"
android:startX="35.09854"
android:startY="81.90271"
android:type="linear">
<item
android:color="@color/logo_gradient_start"
android:offset="0" />
<item
android:color="@color/logo_gradient_stop"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="@color/logo_text"
android:pathData="m189.717,67.665a1.689,1.689 0,0 0,-1.701 1.689l0,38.301a1.691,1.691 0,0 0,2.901 1.21,1.64 1.64,0 0,0 0.478,-1.21l0,-38.301a1.689,1.689 0,0 0,-1.678 -1.689zM202.669,67.665a1.689,1.689 0,0 0,-1.701 1.689l0,38.301a1.691,1.691 0,0 0,2.901 1.21,1.64 1.64,0 0,0 0.478,-1.21l0,-38.301a1.689,1.689 0,0 0,-1.678 -1.689zM255.261,67.89a8.741,8.741 135,0 0,-0.108 0c-2.296,-0.008 -4.128,0.64 -5.499,1.942 -1.37,1.302 -2.055,3.135 -2.055,5.499l0,5.8l-4.111,0a1.579,1.579 0,0 0,-1.127 0.423,1.517 1.517,0 0,0 -0.449,1.155 1.557,1.557 0,0 0,0.449 1.098,1.485 1.485,0 0,0 1.127,0.478l4.111,0l0,23.371a1.689,1.689 0,1 0,3.379 0l0,-23.371l6.305,0a1.559,1.559 0,0 0,1.098 -0.449,1.566 1.566,0 0,0 0,-2.253 1.553,1.553 0,0 0,-1.098 -0.451l-6.309,0l0,-5.8c-0.002,-1.389 0.328,-2.478 0.986,-3.267 0.656,-0.788 1.589,-1.184 2.799,-1.189a7.964,7.964 0,0 1,1.972 0.337,3.567 3.567,0 0,0 1.238,0.281 1.397,1.397 0,0 0,1.014 -0.45,1.53 1.53,0 0,0 0.45,-1.126c0.002,-0.713 -0.449,-1.229 -1.35,-1.549a8.741,8.741 0,0 0,-2.821 -0.478zM144.965,69.916a1.706,1.706 0,0 0,-0.096 0.002,1.813 1.813,0 0,0 -1.295,0.478 1.6,1.6 0,0 0,-0.506 1.212l0,26.25c-0.001,2.591 -0.808,4.685 -2.421,6.28 -1.613,1.595 -3.716,2.393 -6.308,2.393a7.696,7.696 0,0 1,-4.551 -1.437,9.118 9.118,0 0,1 -3.098,-3.857c-0.449,-0.637 -0.977,-0.957 -1.576,-0.957a1.754,1.754 0,0 0,-1.155 0.451,1.407 1.407,0 0,0 -0.537,1.126 1.689,1.689 0,0 0,0.283 0.901,12.704 12.704,0 0,0 4.336,5.209 10.848,10.848 135,0 0,6.306 1.944c2.366,0 4.476,-0.519 6.329,-1.557a11.187,11.187 0,0 0,4.361 -4.308,12.268 12.268,0 0,0 1.578,-6.195l0,-26.242a1.65,1.65 0,0 0,-0.48 -1.212,1.706 1.706,0 0,0 -1.171,-0.48zM266.241,71.552c-0.752,-0.001 -1.344,0.177 -1.775,0.534 -0.431,0.358 -0.647,0.874 -0.647,1.549l0,0.569c0,0.675 0.205,1.193 0.62,1.549 0.406,0.356 0.993,0.537 1.744,0.537s1.316,-0.178 1.718,-0.534 0.591,-0.874 0.591,-1.549l0,-0.569c0,-1.39 -0.751,-2.086 -2.252,-2.086zM167.631,79.893a12.363,12.363 0,0 0,-6.477 1.83c-2.027,1.221 -3.651,2.986 -4.871,5.294s-1.83,5.002 -1.83,8.081c0.001,2.929 0.62,5.509 1.858,7.742a13.18,13.18 0,0 0,5.18 5.21c2.216,1.239 4.769,1.858 7.66,1.858a12.431,12.431 0,0 0,5.766 -1.409,14.164 14.164,0 0,0 4.534,-3.603 1.67,1.67 0,0 0,0.569 -1.183,1.543 1.543,0 0,0 -1.464,-1.464 1.896,1.896 0,0 0,-1.183 0.506,9.992 9.992,0 0,1 -3.436,2.873 10.239,10.239 0,0 1,-4.843 1.126c-2.328,0.001 -4.346,-0.515 -6.054,-1.549a10.411,10.411 0,0 1,-3.942 -4.224c-0.919,-1.782 -1.378,-3.8 -1.378,-6.052l21.286,0a1.663,1.663 0,0 0,1.183 -0.48,1.53 1.53,0 0,0 0.506,-1.159c-0.037,-2.781 -0.675,-5.183 -1.915,-7.205a12.473,12.473 0,0 0,-4.814 -4.619,13.273 13.273,0 0,0 -6.335,-1.574zM290.227,79.893a11.725,11.725 0,0 0,-5.835 1.604c-1.896,1.071 -3.35,2.413 -4.361,4.027l0,-3.098a1.689,1.689 0,1 0,-3.378 0l0,25.235a1.689,1.689 0,1 0,3.378 0l0,-16.388a6.871,6.871 0,0 1,1.352 -4.082,10.102 10.102,0 0,1 8.11,-4.139c2.553,-0.003 4.44,0.756 5.66,2.275 1.22,1.519 1.83,3.5 1.83,5.94l0,16.388a1.638,1.638 0,0 0,1.689 1.689,1.669 1.669,0 0,0 1.183,-0.48 1.593,1.593 0,0 0,0.506 -1.21l0,-16.556c0,-3.378 -0.817,-6.09 -2.45,-8.138 -1.633,-2.045 -4.195,-3.068 -7.685,-3.068zM234.218,80.053a1.706,1.706 0,0 0,-1.714 1.698l0,16.79a7.099,7.099 0,0 1,-1.267 4.027,9.458 9.458,0 0,1 -3.379,3.034 9.101,9.101 0,0 1,-4.421 1.155c-2.514,-0.001 -4.363,-0.696 -5.546,-2.086s-1.775,-3.436 -1.775,-6.14l0,-16.78a1.689,1.689 0,1 0,-3.379 0l0,17.281c0,3.34 0.845,5.986 2.535,7.939 1.69,1.953 4.167,2.93 7.433,2.93a11.86,11.86 135,0 0,5.745 -1.437,10.309 10.309,0 0,0 4.054,-3.792l0,1.968c0.002,3.38 -0.909,6.046 -2.73,7.998 -1.822,1.952 -4.29,2.928 -7.406,2.928 -2.179,-0.001 -4.47,-0.659 -6.871,-1.972a1.5,1.5 0,0 0,-0.675 -0.169,1.42 1.42,0 0,0 -0.844,0.283 1.528,1.528 0,0 0,-0.569 0.844,3.402 3.402,0 0,0 -0.114,0.569c0,0.6 0.394,1.145 1.183,1.635a11.703,11.703 135,0 0,3.413 1.35,18.341 18.341,135 0,0 4.42,0.508c2.59,0 4.918,-0.544 6.983,-1.632a11.676,11.676 0,0 0,4.843 -4.788c1.164,-2.105 1.746,-4.638 1.746,-7.601l0,-24.839a1.65,1.65 0,0 0,-0.48 -1.212,1.706 1.706,0 0,0 -1.186,-0.486zM266.196,80.062a1.689,1.689 0,0 0,-1.701 1.689l0,25.905a1.691,1.691 0,0 0,2.901 1.21l-0.006,0a1.64,1.64 0,0 0,0.484 -1.21l0,-25.905a1.689,1.689 0,0 0,-1.678 -1.689zM167.633,83.158a10.239,10.239 0,0 1,4.361 0.986,9.19 9.19,0 0,1 3.519,2.873 8.895,8.895 0,0 1,1.691 4.427l0,0.451l-19.204,0c0.526,-2.821 1.718,-4.983 3.576,-6.484s3.877,-2.253 6.056,-2.253z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:fillColor="@color/logo_background"
android:pathData="M0,0h256v256h-256z" />
</vector>

View File

@@ -1,3 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"
android:tint="@color/logo_background"></shape>

View File

@@ -1,24 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path android:pathData="m127.998,39.598c-23.398,0 -98.717,136.547 -87.232,159.608s163.108,22.794 174.467,0c11.359,-22.794 -63.838,-159.608 -87.232,-159.608zM128.009,74.617c15.329,0 64.6,89.634 57.157,104.581 -7.442,14.942 -106.782,15.109 -114.312,0s41.827,-104.581 57.157,-104.581zM127.998,107.341c-7.765,0 -32.763,45.317 -28.948,52.969s54.128,7.568 57.898,0 -21.183,-52.966 -28.948,-52.969z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="217.18959"
android:endY="196.4199"
android:startX="70.58595"
android:startY="111.779655"
android:type="linear">
<item
android:color="@color/logo_gradient_start"
android:offset="0" />
<item
android:color="@color/logo_gradient_stop"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</vector>

View File

@@ -1,17 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="493dp"
android:height="154dp"
android:viewportWidth="1480.6"
android:viewportHeight="465.39">
<path android:pathData="m232.73,0c-61.59,0 -259.85,359.43 -229.62,420.13 30.23,60.7 429.34,60 459.24,0s-168.04,-420.13 -229.62,-420.13zM232.76,92.18c40.35,0 170.04,235.94 150.45,275.28 -19.59,39.33 -281.08,39.77 -300.9,0s110.1,-275.28 150.45,-275.28zM232.73,178.32c-20.44,0 -86.24,119.29 -76.2,139.43s142.48,19.92 152.4,0 -55.76,-139.42 -76.2,-139.43z">
android:width="252dp"
android:height="72dp"
android:viewportWidth="252"
android:viewportHeight="72">
<path android:pathData="M24.71,49.16c-1.55,-3.12 8.63,-21.57 11.79,-21.57 3.17,0 13.32,18.49 11.79,21.57 -1.53,3.08 -22.02,3.12 -23.58,0z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="467.455"
android:endY="412.789"
android:startX="81.565"
android:startY="189.999"
android:type="linear">
android:endX="72.5"
android:endY="63"
android:startX="12.5"
android:startY="30"
android:tileMode="clamp">
<item
android:color="@color/logo_gradient_start"
android:offset="0" />
@@ -22,6 +22,25 @@
</aapt:attr>
</path>
<path
android:fillColor="@color/logo_text"
android:pathData="m897.02,114.91a8.91,8.91 0,0 0,-8.97 8.91v202a8.92,8.92 0,0 0,15.3 6.38,8.65 8.65,0 0,0 2.52,-6.38v-202a8.91,8.91 0,0 0,-8.85 -8.91zM965.33,114.91a8.91,8.91 0,0 0,-8.97 8.91v202a8.92,8.92 0,0 0,15.3 6.38,8.65 8.65,0 0,0 2.52,-6.38v-202a8.91,8.91 0,0 0,-8.85 -8.91zM1242.7,116.1a46.1,46.1 0,0 0,-0.57 0c-12.11,-0.04 -21.77,3.37 -29,10.24 -7.23,6.87 -10.84,16.53 -10.84,29v30.59h-21.68a8.33,8.33 0,0 0,-5.94 2.23,8 8,0 0,0 -2.37,6.09 8.21,8.21 0,0 0,2.37 5.79,7.83 7.83,0 0,0 5.94,2.52h21.68v123.26a8.91,8.91 0,1 0,17.82 0v-123.26h33.25a8.22,8.22 0,0 0,5.79 -2.37,8.26 8.26,0 0,0 0,-11.88 8.19,8.19 0,0 0,-5.79 -2.38h-33.27v-30.59c-0.01,-7.33 1.73,-13.07 5.2,-17.23 3.46,-4.15 8.38,-6.24 14.76,-6.27a42,42 0,0 1,10.4 1.78,18.81 18.81,0 0,0 6.53,1.48 7.37,7.37 0,0 0,5.35 -2.37,8.07 8.07,0 0,0 2.37,-5.94c0.01,-3.76 -2.37,-6.48 -7.12,-8.17a46.1,46.1 0,0 0,-14.88 -2.52zM661,126.78a9,9 0,0 0,-0.51 0.01,9.56 9.56,0 0,0 -6.83,2.52 8.44,8.44 0,0 0,-2.67 6.39v138.44c-0.01,13.67 -4.26,24.71 -12.77,33.12 -8.51,8.41 -19.6,12.62 -33.27,12.62a40.59,40.59 0,0 1,-24 -7.58,48.09 48.09,0 0,1 -16.34,-20.34c-2.37,-3.36 -5.15,-5.05 -8.31,-5.05a9.25,9.25 0,0 0,-6.09 2.38,7.42 7.42,0 0,0 -2.83,5.94 8.91,8.91 0,0 0,1.49 4.75,67 67,0 0,0 22.87,27.47 57.21,57.21 0,0 0,33.26 10.25c12.48,0 23.61,-2.74 33.38,-8.21a59,59 0,0 0,23 -22.72,64.7 64.7,0 0,0 8.32,-32.67v-138.4a8.7,8.7 0,0 0,-2.53 -6.39,9 9,0 0,0 -6.17,-2.53zM1300.61,135.41c-3.97,-0.01 -7.09,0.93 -9.36,2.82 -2.27,1.89 -3.41,4.61 -3.41,8.17v3c0,3.56 1.08,6.29 3.27,8.17 2.14,1.88 5.24,2.83 9.2,2.83s6.94,-0.94 9.06,-2.82 3.12,-4.61 3.12,-8.17v-3c0,-7.33 -3.96,-11 -11.88,-11zM780.54,179.4a65.2,65.2 0,0 0,-34.16 9.65c-10.69,6.44 -19.26,15.75 -25.69,27.92s-9.65,26.38 -9.65,42.62c0.01,15.45 3.27,29.06 9.8,40.83a69.51,69.51 0,0 0,27.32 27.48c11.69,6.53 25.15,9.8 40.4,9.8a65.56,65.56 0,0 0,30.41 -7.43,74.7 74.7,0 0,0 23.91,-19 8.81,8.81 0,0 0,3 -6.24,8.14 8.14,0 0,0 -7.72,-7.72 10,10 0,0 0,-6.24 2.67,52.7 52.7,0 0,1 -18.12,15.15 54,54 0,0 1,-25.54 5.94c-12.28,0.01 -22.92,-2.72 -31.93,-8.17a54.91,54.91 0,0 1,-20.79 -22.28c-4.85,-9.4 -7.27,-20.04 -7.27,-31.92h112.26a8.77,8.77 0,0 0,6.24 -2.53,8.07 8.07,0 0,0 2.67,-6.11c-0.19,-14.67 -3.56,-27.33 -10.1,-38a65.78,65.78 0,0 0,-25.39 -24.36,70 70,0 0,0 -33.41,-8.3zM1427.11,179.4a61.84,61.84 0,0 0,-30.77 8.46c-10,5.65 -17.67,12.73 -23,21.24v-16.34a8.91,8.91 0,1 0,-17.82 0v133.09a8.91,8.91 0,1 0,17.82 0v-86.43a36.24,36.24 0,0 1,7.13 -21.53,53.28 53.28,0 0,1 42.77,-21.83c13.47,-0.01 23.42,3.99 29.85,12 6.43,8.01 9.65,18.46 9.65,31.33v86.43a8.64,8.64 0,0 0,8.91 8.91,8.8 8.8,0 0,0 6.24,-2.53 8.4,8.4 0,0 0,2.67 -6.38v-87.32c0,-17.81 -4.31,-32.12 -12.92,-42.92 -8.61,-10.79 -22.12,-16.18 -40.53,-16.18zM1131.72,180.25a9,9 0,0 0,-9.04 8.95v88.55a37.44,37.44 0,0 1,-6.68 21.24,49.88 49.88,0 0,1 -17.82,16 48,48 0,0 1,-23.32 6.09c-13.26,-0.01 -23.01,-3.67 -29.25,-11s-9.36,-18.12 -9.36,-32.38v-88.5a8.91,8.91 0,1 0,-17.82 0v91.14c0,17.61 4.46,31.57 13.37,41.87 8.91,10.3 21.98,15.45 39.2,15.45a62.55,62.55 0,0 0,30.3 -7.58,54.37 54.37,0 0,0 21.38,-20v10.38c0.01,17.83 -4.79,31.89 -14.4,42.18 -9.61,10.29 -22.63,15.44 -39.06,15.44 -11.49,-0.01 -23.57,-3.47 -36.24,-10.4a7.91,7.91 0,0 0,-3.56 -0.89,7.49 7.49,0 0,0 -4.45,1.49 8.06,8.06 0,0 0,-3 4.45,17.94 17.94,0 0,0 -0.6,3c0,3.17 2.08,6.04 6.24,8.62a61.72,61.72 0,0 0,18 7.12,96.73 96.73,0 0,0 23.31,2.68c13.66,0 25.94,-2.87 36.83,-8.61a61.58,61.58 0,0 0,25.54 -25.25c6.14,-11.1 9.21,-24.46 9.21,-40.09v-131a8.7,8.7 0,0 0,-2.53 -6.39,9 9,0 0,0 -6.25,-2.56zM1300.37,180.29a8.91,8.91 0,0 0,-8.97 8.91v136.62a8.92,8.92 0,0 0,15.3 6.38h-0.03a8.65,8.65 0,0 0,2.55 -6.38v-136.62a8.91,8.91 0,0 0,-8.85 -8.91zM780.55,196.62a54,54 0,0 1,23 5.2,48.47 48.47,0 0,1 18.56,15.15 46.91,46.91 0,0 1,8.92 23.35v2.38h-101.28c2.77,-14.88 9.06,-26.28 18.86,-34.2s20.45,-11.88 31.94,-11.88z" />
android:fillType="evenOdd"
android:pathData="M0.98,65C-3.69,55.61 26.98,0 36.5,0c9.53,0 40.15,55.71 35.53,65s-66.37,9.39 -71.04,0m12.26,-8.15c3.07,6.15 43.52,6.08 46.55,0 3.03,-6.09 -17.03,-42.59 -23.27,-42.59S10.17,50.69 13.23,56.85z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="72.5"
android:endY="63"
android:startX="12.5"
android:startY="30"
android:tileMode="clamp">
<item
android:color="@color/logo_gradient_start"
android:offset="0" />
<item
android:color="@color/logo_gradient_stop"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFF"
android:pathData="M143.24,14.25c-0.28,0 -0.42,0 -0.52,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53L142.44,55.2c0,0.28 0,0.42 0.05,0.53 0.05,0.09 0.13,0.17 0.22,0.22 0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L149.67,15.06c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM154.86,14.25c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53L154.06,55.2c0,0.28 0,0.42 0.05,0.53 0.05,0.09 0.12,0.17 0.22,0.22 0.11,0.05 0.25,0.05 0.53,0.05h5.62c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L161.28,15.06c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM208.55,14.25q-5.18,0 -8.27,2.81 -3.09,2.81 -3.09,7.94L197.19,26h-10.17c-0.3,0 -0.45,0 -0.58,0.05a0.75,0.75 0,0 0,-0.29 0.21c-0.09,0.1 -0.14,0.24 -0.25,0.52l-7.43,19.9 -7.48,-19.9c-0.1,-0.28 -0.16,-0.42 -0.25,-0.52a0.76,0.76 0,0 0,-0.3 -0.2c-0.13,-0.05 -0.28,-0.05 -0.58,-0.05h-5.77c-0.39,0 -0.59,0 -0.72,0.08a0.5,0.5 0,0 0,-0.21 0.31c-0.03,0.15 0.04,0.33 0.19,0.7L174.78,56l-0.66,1.6q-0.88,1.87 -2.04,3.03 -1.1,1.16 -3.58,1.16 -0.88,0 -1.88,-0.17a13,13 0,0 1,-0.73 -0.1c-0.39,-0.06 -0.58,-0.1 -0.71,-0.05a0.47,0.47 0,0 0,-0.26 0.22c-0.07,0.12 -0.07,0.3 -0.07,0.66v4.33c0,0.24 0,0.37 0.05,0.48a0.74,0.74 0,0 0,0.19 0.27c0.09,0.08 0.19,0.12 0.39,0.18a8,8 0,0 0,1.47 0.35q1.16,0.22 2.37,0.22 4.24,0 7.06,-2.43 2.87,-2.37 4.58,-6.73l10.52,-26.58h5.72v22.75c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.52,0.05h5.63c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L204.42,32.45h5.87c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L211.09,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.52,-0.05h-5.87v-0.99q0,-2.26 1.32,-3.31 1.38,-1.1 3.75,-1.1 0.46,0 0.94,0.05c0.34,0.03 0.52,0.05 0.63,-0a0.48,0.48 0,0 0,0.24 -0.22c0.06,-0.11 0.06,-0.28 0.06,-0.6v-4.43c0,-0.3 0,-0.46 -0.06,-0.59a0.7,0.7 0,0 0,-0.25 -0.29c-0.12,-0.08 -0.26,-0.1 -0.54,-0.13a14,14 0,0 0,-1.97 -0.13zM99.46,14.92c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.52v27.82q0,2.54 -1.6,4.08 -1.54,1.49 -4.19,1.49h-1.57c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v5.29c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h1.57q4.08,0 7.06,-1.6 3.03,-1.6 4.63,-4.47 1.65,-2.87 1.66,-6.67L106.22,15.72c0,-0.28 -0,-0.42 -0.06,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM213.99,14.92c-0.16,0 -0.26,0.01 -0.34,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.52v6.12c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53v-6.12c0,-0.28 0,-0.42 -0.05,-0.52a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.52,-0.05zM124.56,25.34q-4.19,0 -7.61,2.04 -3.36,2.04 -5.35,5.57 -1.93,3.47 -1.93,8 0,4.36 1.93,7.94c1.93,3.59 3.09,4.28 5.4,5.68q3.47,2.1 8.11,2.1 4.58,0 8,-2.04 3.09,-1.87 4.53,-4.61c0.11,-0.22 0.17,-0.33 0.17,-0.45a0.53,0.53 0,0 0,-0.1 -0.3c-0.07,-0.1 -0.19,-0.16 -0.43,-0.27l-4.27,-2.09c-0.31,-0.15 -0.47,-0.23 -0.61,-0.24a0.6,0.6 0,0 0,-0.35 0.08c-0.12,0.07 -0.24,0.22 -0.48,0.54a8.2,8.2 0,0 1,-2.21 1.99q-1.71,1.05 -4.19,1.05 -3.26,0 -5.46,-1.98 -2.2,-1.99 -2.54,-5.3h21.06c0.2,0 0.3,0 0.39,-0.04a0.55,0.55 0,0 0,0.21 -0.17c0.06,-0.08 0.08,-0.17 0.12,-0.33q0.09,-0.39 0.12,-0.84 0.11,-0.83 0.11,-1.65 0,-4.03 -1.71,-7.33t-4.96,-5.3q-3.26,-2.04 -7.94,-2.04m115.64,0q-2.81,0 -5.07,1.1a7.9,7.9 0,0 0,-3.42 3.25L231.71,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05h-5.18c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v28.4c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.62c0.28,0 0.42,0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53v-16.79q0,-2.92 1.65,-4.69 1.71,-1.77 4.41,-1.77 2.7,0 4.36,1.77 1.71,1.71 1.71,4.69v16.79c0,0.28 -0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.06,-0.25 0.06,-0.53v-18.5q-0,-3.37 -1.44,-5.9a10.1,10.1 0,0 0,-4.03 -4.03q-2.54,-1.43 -5.85,-1.43M214.18,26c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v28.4c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L220.61,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM124.56,31.3q2.87,0 4.74,1.76 1.93,1.71 2.15,4.47h-14.12q0.61,-2.98 2.54,-4.58 1.99,-1.65 4.69,-1.65" />
</vector>

View File

@@ -1,8 +0,0 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="18dp"
android:drawable="@drawable/app_icon_foreground"
android:end="18dp"
android:start="18dp"
android:top="18dp"></item>
</layer-list>

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/app_banner_background" />
<item android:drawable="@drawable/app_banner_foreground" />

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<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"

View File

@@ -1,28 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<path android:pathData="m63.761,45.877c-11.678,0 -49.27,68.152 -43.538,79.661s81.407,11.377 87.076,0 -31.862,-79.661 -43.538,-79.661zM63.766,63.355c7.651,0 32.241,44.737 28.527,52.196 -3.714,7.457 -53.296,7.541 -57.054,0s20.876,-52.196 28.527,-52.196zM63.761,79.688c-3.876,0 -16.352,22.619 -14.448,26.437s27.016,3.777 28.897,0 -10.572,-26.435 -14.448,-26.437z">
<path android:pathData="M59.21,103.16C57.66,100.04 67.84,81.59 71,81.59C74.17,81.59 84.32,100.08 82.79,103.16C81.25,106.24 60.76,106.27 59.21,103.16Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="108.26714"
android:endY="124.14592"
android:startX="35.09854"
android:startY="81.90271"
android:endX="107"
android:endY="117"
android:startX="47"
android:startY="84"
android:type="linear">
<item
android:color="#AA5CC3"
android:color="#FFAA5CC3"
android:offset="0" />
<item
android:color="#00A4DC"
android:color="#FF00A4DC"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:pathData="m189.717,67.665a1.689,1.689 0,0 0,-1.701 1.689l0,38.301a1.691,1.691 0,0 0,2.901 1.21,1.64 1.64,0 0,0 0.478,-1.21l0,-38.301a1.689,1.689 0,0 0,-1.678 -1.689zM202.669,67.665a1.689,1.689 0,0 0,-1.701 1.689l0,38.301a1.691,1.691 0,0 0,2.901 1.21,1.64 1.64,0 0,0 0.478,-1.21l0,-38.301a1.689,1.689 0,0 0,-1.678 -1.689zM255.261,67.89a8.741,8.741 135,0 0,-0.108 0c-2.296,-0.008 -4.128,0.64 -5.499,1.942 -1.37,1.302 -2.055,3.135 -2.055,5.499l0,5.8l-4.111,0a1.579,1.579 0,0 0,-1.127 0.423,1.517 1.517,0 0,0 -0.449,1.155 1.557,1.557 0,0 0,0.449 1.098,1.485 1.485,0 0,0 1.127,0.478l4.111,0l0,23.371a1.689,1.689 0,1 0,3.379 0l0,-23.371l6.305,0a1.559,1.559 0,0 0,1.098 -0.449,1.566 1.566,0 0,0 0,-2.253 1.553,1.553 0,0 0,-1.098 -0.451l-6.309,0l0,-5.8c-0.002,-1.389 0.328,-2.478 0.986,-3.267 0.656,-0.788 1.589,-1.184 2.799,-1.189a7.964,7.964 0,0 1,1.972 0.337,3.567 3.567,0 0,0 1.238,0.281 1.397,1.397 0,0 0,1.014 -0.45,1.53 1.53,0 0,0 0.45,-1.126c0.002,-0.713 -0.449,-1.229 -1.35,-1.549a8.741,8.741 0,0 0,-2.821 -0.478zM144.965,69.916a1.706,1.706 0,0 0,-0.096 0.002,1.813 1.813,0 0,0 -1.295,0.478 1.6,1.6 0,0 0,-0.506 1.212l0,26.25c-0.001,2.591 -0.808,4.685 -2.421,6.28 -1.613,1.595 -3.716,2.393 -6.308,2.393a7.696,7.696 0,0 1,-4.551 -1.437,9.118 9.118,0 0,1 -3.098,-3.857c-0.449,-0.637 -0.977,-0.957 -1.576,-0.957a1.754,1.754 0,0 0,-1.155 0.451,1.407 1.407,0 0,0 -0.537,1.126 1.689,1.689 0,0 0,0.283 0.901,12.704 12.704,0 0,0 4.336,5.209 10.848,10.848 135,0 0,6.306 1.944c2.366,0 4.476,-0.519 6.329,-1.557a11.187,11.187 0,0 0,4.361 -4.308,12.268 12.268,0 0,0 1.578,-6.195l0,-26.242a1.65,1.65 0,0 0,-0.48 -1.212,1.706 1.706,0 0,0 -1.171,-0.48zM266.241,71.552c-0.752,-0.001 -1.344,0.177 -1.775,0.534 -0.431,0.358 -0.647,0.874 -0.647,1.549l0,0.569c0,0.675 0.205,1.193 0.62,1.549 0.406,0.356 0.993,0.537 1.744,0.537s1.316,-0.178 1.718,-0.534 0.591,-0.874 0.591,-1.549l0,-0.569c0,-1.39 -0.751,-2.086 -2.252,-2.086zM167.631,79.893a12.363,12.363 0,0 0,-6.477 1.83c-2.027,1.221 -3.651,2.986 -4.871,5.294s-1.83,5.002 -1.83,8.081c0.001,2.929 0.62,5.509 1.858,7.742a13.18,13.18 0,0 0,5.18 5.21c2.216,1.239 4.769,1.858 7.66,1.858a12.431,12.431 0,0 0,5.766 -1.409,14.164 14.164,0 0,0 4.534,-3.603 1.67,1.67 0,0 0,0.569 -1.183,1.543 1.543,0 0,0 -1.464,-1.464 1.896,1.896 0,0 0,-1.183 0.506,9.992 9.992,0 0,1 -3.436,2.873 10.239,10.239 0,0 1,-4.843 1.126c-2.328,0.001 -4.346,-0.515 -6.054,-1.549a10.411,10.411 0,0 1,-3.942 -4.224c-0.919,-1.782 -1.378,-3.8 -1.378,-6.052l21.286,0a1.663,1.663 0,0 0,1.183 -0.48,1.53 1.53,0 0,0 0.506,-1.159c-0.037,-2.781 -0.675,-5.183 -1.915,-7.205a12.473,12.473 0,0 0,-4.814 -4.619,13.273 13.273,0 0,0 -6.335,-1.574zM290.227,79.893a11.725,11.725 0,0 0,-5.835 1.604c-1.896,1.071 -3.35,2.413 -4.361,4.027l0,-3.098a1.689,1.689 0,1 0,-3.378 0l0,25.235a1.689,1.689 0,1 0,3.378 0l0,-16.388a6.871,6.871 0,0 1,1.352 -4.082,10.102 10.102,0 0,1 8.11,-4.139c2.553,-0.003 4.44,0.756 5.66,2.275 1.22,1.519 1.83,3.5 1.83,5.94l0,16.388a1.638,1.638 0,0 0,1.689 1.689,1.669 1.669,0 0,0 1.183,-0.48 1.593,1.593 0,0 0,0.506 -1.21l0,-16.556c0,-3.378 -0.817,-6.09 -2.45,-8.138 -1.633,-2.045 -4.195,-3.068 -7.685,-3.068zM234.218,80.053a1.706,1.706 0,0 0,-1.714 1.698l0,16.79a7.099,7.099 0,0 1,-1.267 4.027,9.458 9.458,0 0,1 -3.379,3.034 9.101,9.101 0,0 1,-4.421 1.155c-2.514,-0.001 -4.363,-0.696 -5.546,-2.086s-1.775,-3.436 -1.775,-6.14l0,-16.78a1.689,1.689 0,1 0,-3.379 0l0,17.281c0,3.34 0.845,5.986 2.535,7.939 1.69,1.953 4.167,2.93 7.433,2.93a11.86,11.86 135,0 0,5.745 -1.437,10.309 10.309,0 0,0 4.054,-3.792l0,1.968c0.002,3.38 -0.909,6.046 -2.73,7.998 -1.822,1.952 -4.29,2.928 -7.406,2.928 -2.179,-0.001 -4.47,-0.659 -6.871,-1.972a1.5,1.5 0,0 0,-0.675 -0.169,1.42 1.42,0 0,0 -0.844,0.283 1.528,1.528 0,0 0,-0.569 0.844,3.402 3.402,0 0,0 -0.114,0.569c0,0.6 0.394,1.145 1.183,1.635a11.703,11.703 135,0 0,3.413 1.35,18.341 18.341,135 0,0 4.42,0.508c2.59,0 4.918,-0.544 6.983,-1.632a11.676,11.676 0,0 0,4.843 -4.788c1.164,-2.105 1.746,-4.638 1.746,-7.601l0,-24.839a1.65,1.65 0,0 0,-0.48 -1.212,1.706 1.706,0 0,0 -1.186,-0.486zM266.196,80.062a1.689,1.689 0,0 0,-1.701 1.689l0,25.905a1.691,1.691 0,0 0,2.901 1.21l-0.006,0a1.64,1.64 0,0 0,0.484 -1.21l0,-25.905a1.689,1.689 0,0 0,-1.678 -1.689zM167.633,83.158a10.239,10.239 0,0 1,4.361 0.986,9.19 9.19,0 0,1 3.519,2.873 8.895,8.895 0,0 1,1.691 4.427l0,0.451l-19.204,0c0.526,-2.821 1.718,-4.983 3.576,-6.484s3.877,-2.253 6.056,-2.253z" />
android:fillType="evenOdd"
android:pathData="M35.48,119C30.81,109.61 61.48,54 71,54C80.53,54 111.15,109.71 106.53,119C101.9,128.28 40.16,128.39 35.48,119ZM47.74,110.85C50.8,117 91.25,116.93 94.28,110.85C97.31,104.76 77.25,68.26 71.01,68.26C64.77,68.26 44.67,104.69 47.74,110.85Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="107"
android:endY="117"
android:startX="47"
android:startY="84"
android:type="linear">
<item
android:color="#FFAA5CC3"
android:offset="0" />
<item
android:color="#FF00A4DC"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#ffffff"
android:pathData="M260.22,110C259.94,110 259.8,110 259.69,109.95C259.6,109.9 259.52,109.82 259.47,109.73C259.42,109.62 259.42,109.48 259.42,109.2V80.8C259.42,80.52 259.42,80.38 259.47,80.27C259.52,80.18 259.6,80.1 259.69,80.05C259.8,80 259.94,80 260.22,80H265.4C265.68,80 265.82,80 265.93,80.05C266.02,80.1 266.1,80.18 266.15,80.27C266.2,80.38 266.2,80.52 266.2,80.8V83.69C267.01,82.26 268.15,81.18 269.62,80.44C271.13,79.71 272.82,79.34 274.7,79.34C276.9,79.34 278.85,79.82 280.54,80.77C282.27,81.73 283.61,83.07 284.57,84.8C285.52,86.49 286,88.46 286,90.7V109.2C286,109.48 286,109.62 285.95,109.73C285.9,109.82 285.82,109.9 285.73,109.95C285.62,110 285.48,110 285.2,110H279.58C279.3,110 279.16,110 279.05,109.95C278.95,109.9 278.88,109.82 278.83,109.73C278.78,109.62 278.78,109.48 278.78,109.2V92.41C278.78,90.42 278.21,88.86 277.07,87.72C275.96,86.54 274.51,85.96 272.71,85.96C270.91,85.96 269.44,86.54 268.3,87.72C267.2,88.9 266.64,90.46 266.64,92.41V109.2C266.64,109.48 266.64,109.62 266.59,109.73C266.54,109.82 266.46,109.9 266.37,109.95C266.26,110 266.12,110 265.84,110H260.22Z" />
<path
android:fillColor="#ffffff"
android:pathData="M248.68,76.64C248.4,76.64 248.26,76.64 248.15,76.58C248.06,76.53 247.98,76.46 247.93,76.36C247.88,76.26 247.88,76.12 247.88,75.84V69.72C247.88,69.44 247.88,69.3 247.93,69.19C247.98,69.09 248.06,69.02 248.15,68.97C248.26,68.92 248.4,68.92 248.68,68.92H254.3C254.58,68.92 254.72,68.92 254.83,68.97C254.93,69.02 255,69.09 255.05,69.19C255.1,69.3 255.1,69.44 255.1,69.72V75.84C255.1,76.12 255.1,76.26 255.05,76.36C255,76.46 254.93,76.53 254.83,76.58C254.72,76.64 254.58,76.64 254.3,76.64H248.68Z" />
<path
android:fillColor="#ffffff"
android:pathData="M248.68,110C248.4,110 248.26,110 248.15,109.95C248.06,109.9 247.98,109.82 247.93,109.73C247.88,109.62 247.88,109.48 247.88,109.2V80.8C247.88,80.52 247.88,80.38 247.93,80.27C247.98,80.18 248.06,80.1 248.15,80.05C248.26,80 248.4,80 248.68,80H254.3C254.58,80 254.72,80 254.83,80.05C254.93,80.1 255,80.18 255.05,80.27C255.1,80.38 255.1,80.52 255.1,80.8V109.2C255.1,109.48 255.1,109.62 255.05,109.73C255,109.82 254.93,109.9 254.83,109.95C254.72,110 254.58,110 254.3,110H248.68Z" />
<path
android:fillColor="#ffffff"
android:pathData="M231.97,109.95C232.07,110 232.21,110 232.49,110H238.12C238.4,110 238.54,110 238.65,109.95C238.74,109.9 238.82,109.82 238.87,109.73C238.92,109.62 238.92,109.48 238.92,109.2V86.45H244.79C245.07,86.45 245.21,86.45 245.32,86.4C245.41,86.35 245.49,86.27 245.54,86.18C245.59,86.07 245.59,85.93 245.59,85.65V80.8C245.59,80.52 245.59,80.38 245.54,80.27C245.49,80.18 245.41,80.1 245.32,80.05C245.21,80 245.07,80 244.79,80H238.92V79.01C238.92,77.5 239.36,76.4 240.24,75.7C241.16,74.96 242.41,74.6 243.99,74.6C244.3,74.6 244.61,74.61 244.93,74.64C245.28,74.67 245.45,74.69 245.57,74.64C245.67,74.59 245.75,74.52 245.81,74.42C245.87,74.3 245.87,74.14 245.87,73.82V69.39C245.87,69.08 245.87,68.93 245.81,68.8C245.76,68.69 245.66,68.58 245.56,68.52C245.44,68.44 245.3,68.42 245.02,68.38C244.41,68.3 243.75,68.25 243.06,68.25C239.6,68.25 236.84,69.19 234.78,71.07C232.73,72.94 231.7,75.59 231.7,79.01V80H225.2C225.18,80 225.17,80 225.15,80H221.53C221.23,80 221.08,80 220.95,80.05C220.84,80.09 220.74,80.16 220.65,80.25C220.56,80.36 220.51,80.5 220.41,80.78L212.98,100.68L205.49,80.78C205.39,80.5 205.34,80.36 205.24,80.25C205.16,80.16 205.06,80.09 204.95,80.05C204.82,80 204.67,80 204.37,80H198.6C198.21,80 198.01,80 197.88,80.08C197.77,80.16 197.7,80.27 197.67,80.4C197.64,80.54 197.71,80.73 197.86,81.09L209.28,110L208.62,111.6C208.03,112.85 207.35,113.86 206.58,114.63C205.84,115.4 204.65,115.79 202.99,115.79C202.41,115.79 201.78,115.74 201.12,115.63C200.87,115.6 200.62,115.56 200.39,115.52C200,115.46 199.81,115.43 199.68,115.47C199.56,115.52 199.48,115.58 199.42,115.69C199.35,115.81 199.35,115.99 199.35,116.36V120.68C199.35,120.93 199.35,121.05 199.4,121.17C199.44,121.26 199.51,121.36 199.59,121.43C199.68,121.51 199.78,121.55 199.98,121.62C200.44,121.78 200.93,121.89 201.45,121.97C202.22,122.11 203.01,122.19 203.82,122.19C206.65,122.19 209.01,121.38 210.88,119.76C212.79,118.18 214.32,115.94 215.46,113.03L225.98,86.45H231.7V109.2C231.7,109.48 231.7,109.62 231.75,109.73C231.8,109.82 231.87,109.9 231.97,109.95Z" />
<path
android:fillColor="#ffffff"
android:pathData="M189.36,110C189.08,110 188.94,110 188.84,109.95C188.74,109.9 188.66,109.82 188.62,109.73C188.56,109.62 188.56,109.48 188.56,109.2V69.05C188.56,68.77 188.56,68.63 188.62,68.53C188.66,68.43 188.74,68.36 188.84,68.31C188.94,68.25 189.08,68.25 189.36,68.25H194.99C195.27,68.25 195.41,68.25 195.51,68.31C195.61,68.36 195.68,68.43 195.73,68.53C195.79,68.63 195.79,68.77 195.79,69.05V109.2C195.79,109.48 195.79,109.62 195.73,109.73C195.68,109.82 195.61,109.9 195.51,109.95C195.41,110 195.27,110 194.99,110H189.36Z" />
<path
android:fillColor="#ffffff"
android:pathData="M177.74,110C177.46,110 177.32,110 177.22,109.95C177.12,109.9 177.04,109.82 177,109.73C176.94,109.62 176.94,109.48 176.94,109.2V69.05C176.94,68.77 176.94,68.63 177,68.53C177.04,68.43 177.12,68.36 177.22,68.31C177.32,68.25 177.46,68.25 177.74,68.25H183.37C183.65,68.25 183.79,68.25 183.89,68.31C183.99,68.36 184.07,68.43 184.11,68.53C184.17,68.63 184.17,68.77 184.17,69.05V109.2C184.17,109.48 184.17,109.62 184.11,109.73C184.07,109.82 183.99,109.9 183.89,109.95C183.79,110 183.65,110 183.37,110H177.74Z" />
<path
android:fillColor="#ffffff"
android:pathData="M159.61,110.66C156.52,110.66 153.82,109.96 151.5,108.57C149.19,107.17 147.39,105.28 146.1,102.89C144.81,100.5 144.17,97.85 144.17,94.94C144.17,91.93 144.81,89.26 146.1,86.95C147.42,84.6 149.21,82.74 151.45,81.38C153.73,80.02 156.26,79.34 159.06,79.34C162.18,79.34 164.83,80.02 167,81.38C169.17,82.7 170.82,84.47 171.96,86.67C173.1,88.88 173.67,91.32 173.67,94.01C173.67,94.56 173.64,95.11 173.56,95.66C173.54,95.96 173.5,96.24 173.44,96.5C173.4,96.67 173.39,96.75 173.33,96.83C173.28,96.9 173.19,96.97 173.12,97C173.02,97.04 172.93,97.04 172.73,97.04H151.67C151.89,99.25 152.74,101.01 154.21,102.33C155.68,103.66 157.5,104.32 159.66,104.32C161.32,104.32 162.72,103.97 163.86,103.27C164.74,102.72 165.47,102.06 166.06,101.29C166.3,100.97 166.42,100.81 166.55,100.74C166.66,100.68 166.76,100.66 166.9,100.66C167.04,100.67 167.19,100.74 167.51,100.9L171.77,102.99C172.01,103.11 172.13,103.17 172.21,103.27C172.27,103.35 172.3,103.46 172.3,103.56C172.3,103.69 172.25,103.8 172.13,104.01C171.18,105.84 169.67,107.38 167.61,108.62C165.33,109.98 162.66,110.66 159.61,110.66ZM159.06,85.29C157.26,85.29 155.69,85.85 154.37,86.95C153.08,88.01 152.24,89.54 151.83,91.53H165.95C165.8,89.69 165.09,88.2 163.8,87.06C162.55,85.88 160.97,85.29 159.06,85.29Z" />
<path
android:fillColor="#ffffff"
android:pathData="M125.8,110C125.52,110 125.38,110 125.27,109.95C125.18,109.9 125.1,109.82 125.06,109.73C125,109.62 125,109.48 125,109.2V103.91C125,103.63 125,103.49 125.06,103.38C125.1,103.29 125.18,103.21 125.27,103.16C125.38,103.11 125.52,103.11 125.8,103.11H127.37C129.14,103.11 130.53,102.61 131.56,101.62C132.63,100.59 133.16,99.23 133.16,97.54V69.72C133.16,69.44 133.16,69.3 133.22,69.19C133.26,69.09 133.34,69.02 133.44,68.97C133.54,68.92 133.68,68.92 133.96,68.92H139.92C140.2,68.92 140.34,68.92 140.45,68.97C140.54,69.02 140.62,69.09 140.66,69.19C140.72,69.3 140.72,69.44 140.72,69.72V97.26C140.72,99.8 140.17,102.02 139.06,103.93C138,105.85 136.45,107.33 134.43,108.4C132.45,109.47 130.09,110 127.37,110H125.8Z" />
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/app_icon_background" />
<item android:drawable="@drawable/app_icon_foreground" />
</layer-list>

View File

@@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#000B25"
android:pathData="M0,0h256v256h-256z" />
android:pathData="M0,0h108v108h-108z" />
</vector>

View File

@@ -1,3 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"
android:tint="#000B25"></shape>

View File

@@ -1,22 +1,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path android:pathData="m127.998,39.598c-23.398,0 -98.717,136.547 -87.232,159.608s163.108,22.794 174.467,0c11.359,-22.794 -63.838,-159.608 -87.232,-159.608zM128.009,74.617c15.329,0 64.6,89.634 57.157,104.581 -7.442,14.942 -106.782,15.109 -114.312,0s41.827,-104.581 57.157,-104.581zM127.998,107.341c-7.765,0 -32.763,45.317 -28.948,52.969s54.128,7.568 57.898,0 -21.183,-52.966 -28.948,-52.969z">
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M47.45,59.31C46.59,57.58 52.24,47.33 54,47.33C55.76,47.33 61.4,57.6 60.55,59.31C59.7,61.02 48.31,61.04 47.45,59.31Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="217.18959"
android:endY="196.4199"
android:startX="70.58595"
android:startY="111.779655"
android:endX="74"
android:endY="67"
android:startX="40.67"
android:startY="48.67"
android:type="linear">
<item
android:color="#AA5CC3"
android:color="#FFAA5CC3"
android:offset="0" />
<item
android:color="#00A4DC"
android:color="#FF00A4DC"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillType="evenOdd"
android:pathData="M34.27,68.11C31.67,62.89 48.71,32 54,32C59.3,32 76.31,62.95 73.74,68.11C71.17,73.27 36.87,73.33 34.27,68.11ZM41.08,63.58C42.78,67 65.25,66.96 66.94,63.58C68.62,60.2 57.47,39.92 54.01,39.92C50.54,39.92 39.37,60.16 41.08,63.58Z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="74"
android:endY="67"
android:startX="40.67"
android:startY="48.67"
android:type="linear">
<item
android:color="#FFAA5CC3"
android:offset="0" />
<item
android:color="#FF00A4DC"
android:offset="1" />
</gradient>
</aapt:attr>

View File

@@ -0,0 +1,13 @@
<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">
<path
android:fillColor="#FFFFFF"
android:pathData="M47.45,59.31C46.59,57.58 52.24,47.33 54,47.33C55.76,47.33 61.4,57.6 60.55,59.31C59.7,61.02 48.31,61.04 47.45,59.31Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M34.27,68.11C31.67,62.89 48.71,32 54,32C59.3,32 76.31,62.95 73.74,68.11C71.17,73.27 36.87,73.33 34.27,68.11ZM41.08,63.58C42.78,67 65.25,66.96 66.94,63.58C68.62,60.2 57.47,39.92 54.01,39.92C50.54,39.92 39.37,60.16 41.08,63.58Z" />
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/app_icon_background_round" />
<item android:drawable="@drawable/app_icon_foreground" />
</layer-list>

View File

@@ -1,17 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="493dp"
android:height="154dp"
android:viewportWidth="1480.6"
android:viewportHeight="465.39">
<path android:pathData="m232.73,0c-61.59,0 -259.85,359.43 -229.62,420.13 30.23,60.7 429.34,60 459.24,0s-168.04,-420.13 -229.62,-420.13zM232.76,92.18c40.35,0 170.04,235.94 150.45,275.28 -19.59,39.33 -281.08,39.77 -300.9,0s110.1,-275.28 150.45,-275.28zM232.73,178.32c-20.44,0 -86.24,119.29 -76.2,139.43s142.48,19.92 152.4,0 -55.76,-139.42 -76.2,-139.43z">
android:width="252dp"
android:height="72dp"
android:viewportWidth="252"
android:viewportHeight="72">
<path android:pathData="M24.71,49.16c-1.55,-3.12 8.63,-21.57 11.79,-21.57 3.17,0 13.32,18.49 11.79,21.57 -1.53,3.08 -22.02,3.12 -23.58,0z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="467.455"
android:endY="412.789"
android:startX="81.565"
android:startY="189.999"
android:type="linear">
android:endX="72.5"
android:endY="63"
android:startX="12.5"
android:startY="30"
android:tileMode="clamp">
<item
android:color="#AA5CC3"
android:offset="0" />
@@ -22,6 +22,25 @@
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:pathData="m897.02,114.91a8.91,8.91 0,0 0,-8.97 8.91v202a8.92,8.92 0,0 0,15.3 6.38,8.65 8.65,0 0,0 2.52,-6.38v-202a8.91,8.91 0,0 0,-8.85 -8.91zM965.33,114.91a8.91,8.91 0,0 0,-8.97 8.91v202a8.92,8.92 0,0 0,15.3 6.38,8.65 8.65,0 0,0 2.52,-6.38v-202a8.91,8.91 0,0 0,-8.85 -8.91zM1242.7,116.1a46.1,46.1 0,0 0,-0.57 0c-12.11,-0.04 -21.77,3.37 -29,10.24 -7.23,6.87 -10.84,16.53 -10.84,29v30.59h-21.68a8.33,8.33 0,0 0,-5.94 2.23,8 8,0 0,0 -2.37,6.09 8.21,8.21 0,0 0,2.37 5.79,7.83 7.83,0 0,0 5.94,2.52h21.68v123.26a8.91,8.91 0,1 0,17.82 0v-123.26h33.25a8.22,8.22 0,0 0,5.79 -2.37,8.26 8.26,0 0,0 0,-11.88 8.19,8.19 0,0 0,-5.79 -2.38h-33.27v-30.59c-0.01,-7.33 1.73,-13.07 5.2,-17.23 3.46,-4.15 8.38,-6.24 14.76,-6.27a42,42 0,0 1,10.4 1.78,18.81 18.81,0 0,0 6.53,1.48 7.37,7.37 0,0 0,5.35 -2.37,8.07 8.07,0 0,0 2.37,-5.94c0.01,-3.76 -2.37,-6.48 -7.12,-8.17a46.1,46.1 0,0 0,-14.88 -2.52zM661,126.78a9,9 0,0 0,-0.51 0.01,9.56 9.56,0 0,0 -6.83,2.52 8.44,8.44 0,0 0,-2.67 6.39v138.44c-0.01,13.67 -4.26,24.71 -12.77,33.12 -8.51,8.41 -19.6,12.62 -33.27,12.62a40.59,40.59 0,0 1,-24 -7.58,48.09 48.09,0 0,1 -16.34,-20.34c-2.37,-3.36 -5.15,-5.05 -8.31,-5.05a9.25,9.25 0,0 0,-6.09 2.38,7.42 7.42,0 0,0 -2.83,5.94 8.91,8.91 0,0 0,1.49 4.75,67 67,0 0,0 22.87,27.47 57.21,57.21 0,0 0,33.26 10.25c12.48,0 23.61,-2.74 33.38,-8.21a59,59 0,0 0,23 -22.72,64.7 64.7,0 0,0 8.32,-32.67v-138.4a8.7,8.7 0,0 0,-2.53 -6.39,9 9,0 0,0 -6.17,-2.53zM1300.61,135.41c-3.97,-0.01 -7.09,0.93 -9.36,2.82 -2.27,1.89 -3.41,4.61 -3.41,8.17v3c0,3.56 1.08,6.29 3.27,8.17 2.14,1.88 5.24,2.83 9.2,2.83s6.94,-0.94 9.06,-2.82 3.12,-4.61 3.12,-8.17v-3c0,-7.33 -3.96,-11 -11.88,-11zM780.54,179.4a65.2,65.2 0,0 0,-34.16 9.65c-10.69,6.44 -19.26,15.75 -25.69,27.92s-9.65,26.38 -9.65,42.62c0.01,15.45 3.27,29.06 9.8,40.83a69.51,69.51 0,0 0,27.32 27.48c11.69,6.53 25.15,9.8 40.4,9.8a65.56,65.56 0,0 0,30.41 -7.43,74.7 74.7,0 0,0 23.91,-19 8.81,8.81 0,0 0,3 -6.24,8.14 8.14,0 0,0 -7.72,-7.72 10,10 0,0 0,-6.24 2.67,52.7 52.7,0 0,1 -18.12,15.15 54,54 0,0 1,-25.54 5.94c-12.28,0.01 -22.92,-2.72 -31.93,-8.17a54.91,54.91 0,0 1,-20.79 -22.28c-4.85,-9.4 -7.27,-20.04 -7.27,-31.92h112.26a8.77,8.77 0,0 0,6.24 -2.53,8.07 8.07,0 0,0 2.67,-6.11c-0.19,-14.67 -3.56,-27.33 -10.1,-38a65.78,65.78 0,0 0,-25.39 -24.36,70 70,0 0,0 -33.41,-8.3zM1427.11,179.4a61.84,61.84 0,0 0,-30.77 8.46c-10,5.65 -17.67,12.73 -23,21.24v-16.34a8.91,8.91 0,1 0,-17.82 0v133.09a8.91,8.91 0,1 0,17.82 0v-86.43a36.24,36.24 0,0 1,7.13 -21.53,53.28 53.28,0 0,1 42.77,-21.83c13.47,-0.01 23.42,3.99 29.85,12 6.43,8.01 9.65,18.46 9.65,31.33v86.43a8.64,8.64 0,0 0,8.91 8.91,8.8 8.8,0 0,0 6.24,-2.53 8.4,8.4 0,0 0,2.67 -6.38v-87.32c0,-17.81 -4.31,-32.12 -12.92,-42.92 -8.61,-10.79 -22.12,-16.18 -40.53,-16.18zM1131.72,180.25a9,9 0,0 0,-9.04 8.95v88.55a37.44,37.44 0,0 1,-6.68 21.24,49.88 49.88,0 0,1 -17.82,16 48,48 0,0 1,-23.32 6.09c-13.26,-0.01 -23.01,-3.67 -29.25,-11s-9.36,-18.12 -9.36,-32.38v-88.5a8.91,8.91 0,1 0,-17.82 0v91.14c0,17.61 4.46,31.57 13.37,41.87 8.91,10.3 21.98,15.45 39.2,15.45a62.55,62.55 0,0 0,30.3 -7.58,54.37 54.37,0 0,0 21.38,-20v10.38c0.01,17.83 -4.79,31.89 -14.4,42.18 -9.61,10.29 -22.63,15.44 -39.06,15.44 -11.49,-0.01 -23.57,-3.47 -36.24,-10.4a7.91,7.91 0,0 0,-3.56 -0.89,7.49 7.49,0 0,0 -4.45,1.49 8.06,8.06 0,0 0,-3 4.45,17.94 17.94,0 0,0 -0.6,3c0,3.17 2.08,6.04 6.24,8.62a61.72,61.72 0,0 0,18 7.12,96.73 96.73,0 0,0 23.31,2.68c13.66,0 25.94,-2.87 36.83,-8.61a61.58,61.58 0,0 0,25.54 -25.25c6.14,-11.1 9.21,-24.46 9.21,-40.09v-131a8.7,8.7 0,0 0,-2.53 -6.39,9 9,0 0,0 -6.25,-2.56zM1300.37,180.29a8.91,8.91 0,0 0,-8.97 8.91v136.62a8.92,8.92 0,0 0,15.3 6.38h-0.03a8.65,8.65 0,0 0,2.55 -6.38v-136.62a8.91,8.91 0,0 0,-8.85 -8.91zM780.55,196.62a54,54 0,0 1,23 5.2,48.47 48.47,0 0,1 18.56,15.15 46.91,46.91 0,0 1,8.92 23.35v2.38h-101.28c2.77,-14.88 9.06,-26.28 18.86,-34.2s20.45,-11.88 31.94,-11.88z" />
android:fillType="evenOdd"
android:pathData="M0.98,65C-3.69,55.61 26.98,0 36.5,0c9.53,0 40.15,55.71 35.53,65s-66.37,9.39 -71.04,0m12.26,-8.15c3.07,6.15 43.52,6.08 46.55,0 3.03,-6.09 -17.03,-42.59 -23.27,-42.59S10.17,50.69 13.23,56.85z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="72.5"
android:endY="63"
android:startX="12.5"
android:startY="30"
android:tileMode="clamp">
<item
android:color="#AA5CC3"
android:offset="0" />
<item
android:color="#00A4DC"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFF"
android:pathData="M143.24,14.25c-0.28,0 -0.42,0 -0.52,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53L142.44,55.2c0,0.28 0,0.42 0.05,0.53 0.05,0.09 0.13,0.17 0.22,0.22 0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L149.67,15.06c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM154.86,14.25c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53L154.06,55.2c0,0.28 0,0.42 0.05,0.53 0.05,0.09 0.12,0.17 0.22,0.22 0.11,0.05 0.25,0.05 0.53,0.05h5.62c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L161.28,15.06c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM208.55,14.25q-5.18,0 -8.27,2.81 -3.09,2.81 -3.09,7.94L197.19,26h-10.17c-0.3,0 -0.45,0 -0.58,0.05a0.75,0.75 0,0 0,-0.29 0.21c-0.09,0.1 -0.14,0.24 -0.25,0.52l-7.43,19.9 -7.48,-19.9c-0.1,-0.28 -0.16,-0.42 -0.25,-0.52a0.76,0.76 0,0 0,-0.3 -0.2c-0.13,-0.05 -0.28,-0.05 -0.58,-0.05h-5.77c-0.39,0 -0.59,0 -0.72,0.08a0.5,0.5 0,0 0,-0.21 0.31c-0.03,0.15 0.04,0.33 0.19,0.7L174.78,56l-0.66,1.6q-0.88,1.87 -2.04,3.03 -1.1,1.16 -3.58,1.16 -0.88,0 -1.88,-0.17a13,13 0,0 1,-0.73 -0.1c-0.39,-0.06 -0.58,-0.1 -0.71,-0.05a0.47,0.47 0,0 0,-0.26 0.22c-0.07,0.12 -0.07,0.3 -0.07,0.66v4.33c0,0.24 0,0.37 0.05,0.48a0.74,0.74 0,0 0,0.19 0.27c0.09,0.08 0.19,0.12 0.39,0.18a8,8 0,0 0,1.47 0.35q1.16,0.22 2.37,0.22 4.24,0 7.06,-2.43 2.87,-2.37 4.58,-6.73l10.52,-26.58h5.72v22.75c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.52,0.05h5.63c0.28,0 0.42,-0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L204.42,32.45h5.87c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L211.09,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.52,-0.05h-5.87v-0.99q0,-2.26 1.32,-3.31 1.38,-1.1 3.75,-1.1 0.46,0 0.94,0.05c0.34,0.03 0.52,0.05 0.63,-0a0.48,0.48 0,0 0,0.24 -0.22c0.06,-0.11 0.06,-0.28 0.06,-0.6v-4.43c0,-0.3 0,-0.46 -0.06,-0.59a0.7,0.7 0,0 0,-0.25 -0.29c-0.12,-0.08 -0.26,-0.1 -0.54,-0.13a14,14 0,0 0,-1.97 -0.13zM99.46,14.92c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.52v27.82q0,2.54 -1.6,4.08 -1.54,1.49 -4.19,1.49h-1.57c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v5.29c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h1.57q4.08,0 7.06,-1.6 3.03,-1.6 4.63,-4.47 1.65,-2.87 1.66,-6.67L106.22,15.72c0,-0.28 -0,-0.42 -0.06,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM213.99,14.92c-0.16,0 -0.26,0.01 -0.34,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.52v6.12c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53v-6.12c0,-0.28 0,-0.42 -0.05,-0.52a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.52,-0.05zM124.56,25.34q-4.19,0 -7.61,2.04 -3.36,2.04 -5.35,5.57 -1.93,3.47 -1.93,8 0,4.36 1.93,7.94c1.93,3.59 3.09,4.28 5.4,5.68q3.47,2.1 8.11,2.1 4.58,0 8,-2.04 3.09,-1.87 4.53,-4.61c0.11,-0.22 0.17,-0.33 0.17,-0.45a0.53,0.53 0,0 0,-0.1 -0.3c-0.07,-0.1 -0.19,-0.16 -0.43,-0.27l-4.27,-2.09c-0.31,-0.15 -0.47,-0.23 -0.61,-0.24a0.6,0.6 0,0 0,-0.35 0.08c-0.12,0.07 -0.24,0.22 -0.48,0.54a8.2,8.2 0,0 1,-2.21 1.99q-1.71,1.05 -4.19,1.05 -3.26,0 -5.46,-1.98 -2.2,-1.99 -2.54,-5.3h21.06c0.2,0 0.3,0 0.39,-0.04a0.55,0.55 0,0 0,0.21 -0.17c0.06,-0.08 0.08,-0.17 0.12,-0.33q0.09,-0.39 0.12,-0.84 0.11,-0.83 0.11,-1.65 0,-4.03 -1.71,-7.33t-4.96,-5.3q-3.26,-2.04 -7.94,-2.04m115.64,0q-2.81,0 -5.07,1.1a7.9,7.9 0,0 0,-3.42 3.25L231.71,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05h-5.18c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v28.4c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.62c0.28,0 0.42,0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53v-16.79q0,-2.92 1.65,-4.69 1.71,-1.77 4.41,-1.77 2.7,0 4.36,1.77 1.71,1.71 1.71,4.69v16.79c0,0.28 -0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.53,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.06,-0.25 0.06,-0.53v-18.5q-0,-3.37 -1.44,-5.9a10.1,10.1 0,0 0,-4.03 -4.03q-2.54,-1.43 -5.85,-1.43M214.18,26c-0.28,0 -0.42,0 -0.53,0.05a0.5,0.5 0,0 0,-0.22 0.22c-0.05,0.11 -0.05,0.25 -0.05,0.53v28.4c0,0.28 0,0.42 0.05,0.53a0.5,0.5 0,0 0,0.22 0.22c0.11,0.05 0.25,0.05 0.53,0.05h5.63c0.28,0 0.42,0 0.52,-0.05a0.5,0.5 0,0 0,0.22 -0.22c0.05,-0.11 0.05,-0.25 0.05,-0.53L220.61,26.8c0,-0.28 0,-0.42 -0.05,-0.53a0.5,0.5 0,0 0,-0.22 -0.22c-0.11,-0.05 -0.25,-0.05 -0.53,-0.05zM124.56,31.3q2.87,0 4.74,1.76 1.93,1.71 2.15,4.47h-14.12q0.61,-2.98 2.54,-4.58 1.99,-1.65 4.69,-1.65" />
</vector>

View File

@@ -48,9 +48,8 @@
app:layout_constraintTop_toTopOf="@id/logo" />
<!-- Fragment showing search results -->
<androidx.fragment.app.FragmentContainerView
<FrameLayout
android:id="@+id/results_frame"
android:name="androidx.leanback.app.RowsSupportFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="27dp"

View File

@@ -20,13 +20,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/saved_servers"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/stored_servers"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:ignore="NestedWeights"
tools:itemCount="3"
tools:listitem="@layout/view_button_server" />

View File

@@ -31,7 +31,8 @@
style="@style/TextAppearance.AppCompat.Display3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
android:layout_marginTop="16dp"
android:textDirection="ltr" />
<ProgressBar
android:id="@+id/loading"

View File

@@ -12,6 +12,7 @@
MODIFIED to add Jellyfin specifics
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:lb="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="androidx.leanback.widget.BaseCardView">
@@ -26,6 +27,7 @@
tools:ignore="UnusedAttribute"
tools:visibility="visible">
<!-- Crossfading is broken with this specific layout, especially when using thumbnails -->
<org.jellyfin.androidtv.ui.AsyncImageView
android:id="@+id/main_image"
android:layout_width="wrap_content"
@@ -33,6 +35,7 @@
android:background="@drawable/shape_card_image_background"
android:contentDescription="@null"
android:scaleType="centerCrop"
app:crossfadeDuration="0"
lb:layout_viewType="main"
tools:src="@drawable/app_logo" />

View File

@@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
@@ -31,7 +30,7 @@
</FrameLayout>
<com.google.android.exoplayer2.ui.StyledPlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/exoPlayerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -55,6 +54,7 @@
android:gravity="center"
android:textColor="@color/white"
android:textSize="28sp"
android:textDirection="ltr"
app:strokeWidth="5.0"
tools:text="Subtitles" />

View File

@@ -1,5 +1,6 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/app_icon_background" />
<foreground android:drawable="@drawable/app_icon_foreground_padded" />
<monochrome android:drawable="@drawable/app_icon_foreground_padded" />
<foreground android:drawable="@drawable/app_icon_foreground" />
<monochrome android:drawable="@drawable/app_icon_foreground_monochrome" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -26,6 +26,7 @@
<attr name="controlIconBackground" format="reference|color" />
<attr name="controlIconForegroundActive" format="reference|color" />
<attr name="controlIconForegroundInactive" format="reference|color" />
<attr name="defaultBackground" format="reference|color" />
<!-- QR drawables -->
<attr name="qrBackground" format="color" />
<attr name="qrForeground" format="color" />

View File

@@ -476,7 +476,7 @@
<string name="crash_report_toast">Oops! Something went wrong, a crash report was sent to your Jellyfin server.</string>
<string name="server_setup_incomplete">The setup of this server has not been completed. Open Jellyfin in a web browser to finish setup before signing in.</string>
<string name="episode_name_special">special</string>
<string name="app_notification_beta">App updated to %1$s\nThank you for participating in the Jellyfin beta program</string>
<string name="app_notification_beta">App updated to %1$s\nThank you for participating in the Jellyfin beta program.</string>
<string name="item_deleted">%1$s deleted</string>
<string name="item_deletion_failed">Unable to delete %1$s</string>
<string name="pref_enable_media_management">Enable media management</string>
@@ -494,6 +494,32 @@
<string name="pref_music_rewrite_disable">Disable new music backend</string>
<string name="pref_music_rewrite_enable_description">Only disable this if you\'re experiencing issues with music playback</string>
<string name="disable_ui_mode_warning">Disable device type warning</string>
<string name="channels">Channels</string>
<string name="photo_albums">Photo albums</string>
<string name="photos">Photos</string>
<string name="person_birthday_and_age">Born %1$s (%2$d)</string>
<string name="dolby_vision">Dolby Vision</string>
<string name="indicator_subtitles">CC</string>
<string name="dolby_atmos">Dolby Atmos</string>
<string name="dts_x">DTS:X</string>
<string name="dts_hd">DTS:HD</string>
<string name="dca">DTS</string>
<string name="ac3">DD</string>
<string name="eac3">DD+</string>
<string name="clear_image_cache">Clear image cache</string>
<string name="clear_image_cache_content">Used: %1$s</string>
<string name="random">Random</string>
<string name="unreleased">Not yet released</string>
<string name="pref_playback_advanced">Advanced playback preferences</string>
<string name="pref_screensaver_ageratingmax">Maximum age rating</string>
<string name="pref_screensaver_ageratingmax_zero">All ages</string>
<string name="pref_screensaver_ageratingmax_entry">Up to age %1$d</string>
<string name="pref_screensaver_ageratingmax_unlimited">Disable maximum</string>
<string name="pref_screensaver_ageratingrequired_title">Require age rating</string>
<string name="pref_screensaver_ageratingrequired_enabled">Only showing items with an age rating set</string>
<string name="pref_screensaver_ageratingrequired_disabled">Showing all items</string>
<string name="all_channels">All channels</string>
<string name="app_notification_update_soon">Update your server from %1$s to at least %2$s to continue using the app after the next update.</string>
<plurals name="seconds">
<item quantity="one">%1$s second</item>
<item quantity="other">%1$s seconds</item>

View File

@@ -9,7 +9,7 @@
<item name="android:colorPrimary">@color/theme_emerald_dark</item>
<item name="android:colorPrimaryDark">@color/theme_emerald_dark</item>
<item name="android:colorAccent">@color/theme_emerald_light</item>
<item name="android:windowBackground">@drawable/moviebg</item>
<item name="defaultBackground">@drawable/moviebg</item>
<item name="defaultSearchColor">@color/theme_emerald_light</item>
<item name="progressPrimary">@color/theme_emerald_light</item>
<item name="progressSecondary">@color/theme_emerald_dark</item>

View File

@@ -7,7 +7,8 @@
<item name="android:colorPrimaryDark">@color/grey</item>
<item name="android:colorAccent">@color/jellyfin_blue</item>
<!-- Background resource or color -->
<item name="android:windowBackground">@color/not_quite_black</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="defaultBackground">@color/not_quite_black</item>
<!-- Background color for CardViews -->
<item name="cardImageBackground">@color/grey</item>
<item name="cardViewBackground">@android:color/transparent</item>
@@ -87,7 +88,6 @@
</style>
<style name="Theme.Jellyfin.Preferences" parent="Theme.Jellyfin">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowAnimationStyle">@style/WindowAnimation.SlideRight</item>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/app_id"
android:icon="@drawable/app_icon"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:smallIcon="@drawable/app_icon" />
android:smallIcon="@mipmap/app_icon" />

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/searchable_hint"
android:icon="@drawable/app_icon_foreground_padded"
android:icon="@mipmap/app_icon"
android:imeOptions="actionSearch"
android:includeInGlobalSearch="true"
android:label="@string/app_name"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -15,7 +15,7 @@ androidx-core = "1.12.0"
androidx-fragment = "1.6.2"
androidx-leanback = "1.1.0-rc01"
androidx-lifecycle = "2.6.2"
androidx-media3 = "1.2.0"
androidx-media3 = "1.3.1"
androidx-preference = "1.2.1"
androidx-recyclerview = "1.3.2"
androidx-startup = "1.1.1"
@@ -25,11 +25,10 @@ androidx-work = "2.9.0"
blurhash = "0.2.0"
coil = "2.5.0"
detekt = "1.23.4"
exoplayer = "2.19.1"
gson = "2.8.9"
jellyfin-androidx-media = "1.2.1+1"
jellyfin-apiclient = "v0.7.10"
jellyfin-exoplayer-ffmpegextension = "2.19.1+1"
jellyfin-sdk = "1.4.6"
jellyfin-sdk = "1.4.7"
junit = "4.13.2"
koin = "3.5.0"
koin-compose = "3.5.0"
@@ -79,6 +78,7 @@ androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt
androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "androidx-media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" }
androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerview" }
androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" }
@@ -93,8 +93,9 @@ koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", versi
koin-androidx-workmanager = { module = "io.insert-koin:koin-androidx-workmanager", version.ref = "koin" }
# Media players
exoplayer = { module = "com.google.android.exoplayer:exoplayer", version.ref = "exoplayer" }
jellyfin-exoplayer-ffmpegextension = { group = "org.jellyfin.exoplayer", name = "exoplayer-ffmpeg-extension", version.ref = "jellyfin-exoplayer-ffmpegextension" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
androidx-media3-exoplayer-hls = { module = "androidx.media3:media3-exoplayer-hls", version.ref = "androidx-media3" }
jellyfin-androidx-media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version.ref = "jellyfin-androidx-media" }
libvlc = { module = "org.videolan.android:libvlc-all", version.ref = "libvlc" }
# Markwon

View File

@@ -93,14 +93,15 @@ internal class MediaSessionPlayer(
val previous = state.queue.peekPrevious()
val next = state.queue.peekNext()
listOfNotNull(previous, current, next)
val playlist = listOfNotNull(previous, current, next)
.distinctBy { it.metadata.mediaId }
.map {
MediaItemData.Builder(requireNotNull(it.metadata.mediaId)).apply {
setMediaItem(it.metadata.toMediaItem())
setDurationUs(it.metadata.duration?.inWholeMicroseconds ?: C.TIME_UNSET)
}.build()
}.let(::setPlaylist)
}
setPlaylist(playlist)
setPlaybackState(when (state.playState.value) {
PlayState.STOPPED -> STATE_IDLE
@@ -109,7 +110,7 @@ internal class MediaSessionPlayer(
PlayState.ERROR -> STATE_ENDED
})
setCurrentMediaItemIndex(if (previous == null) 0 else 1)
setCurrentMediaItemIndex(if (previous == null || playlist.size <= 1) 0 else 1)
} else {
setPlaybackState(STATE_IDLE)
setCurrentMediaItemIndex(C.INDEX_UNSET)

View File

@@ -91,18 +91,14 @@ class DefaultPlayerQueueState(
override fun replaceQueue(queue: Queue) {
Timber.d("Queue changed, setting index to 0")
_current.value = queue
orderIndexProvider.reset()
if (orderIndexProvider != defaultOrderIndexProvider) defaultOrderIndexProvider.reset()
currentQueueIndicesPlayed.clear()
coroutineScope.launch {
when (state.playbackOrder.value) {
PlaybackOrder.DEFAULT -> setIndex(0)
PlaybackOrder.RANDOM,
PlaybackOrder.SHUFFLE -> setIndex((0 until queue.size).random())
}
_current.value = queue
orderIndexProvider.reset()
if (orderIndexProvider != defaultOrderIndexProvider) defaultOrderIndexProvider.reset()
currentQueueIndicesPlayed.clear()
setIndex(0)
}
}

View File

@@ -20,13 +20,13 @@ internal class ShuffleOrderIndexProvider : OrderIndexProvider {
} else {
val remainingIndices = (0..queue.size).filterNot {
it in playedIndices || it in nextIndices
}.shuffled()
}
List(min(amount, remainingItemsSize)) { i ->
if (i <= nextIndices.lastIndex) {
if (i < nextIndices.lastIndex) {
nextIndices[i]
} else {
val index = remainingIndices[i - nextIndices.size]
val index = remainingIndices.random()
nextIndices.add(index)
index
}

View File

@@ -29,9 +29,12 @@ dependencies {
implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.coroutines.guava)
// AndroidX
implementation(libs.androidx.core)
// ExoPlayer
implementation(libs.exoplayer)
implementation(libs.jellyfin.exoplayer.ffmpegextension)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.jellyfin.androidx.media3.ffmpeg.decoder)
// Logging
implementation(libs.timber)

View File

@@ -1,13 +1,22 @@
package org.jellyfin.playback.exoplayer
import android.app.ActivityManager
import android.content.Context
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.video.VideoSize
import androidx.annotation.OptIn
import androidx.core.content.getSystemService
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.ts.TsExtractor
import org.jellyfin.playback.core.backend.BasePlayerBackend
import org.jellyfin.playback.core.mediastream.MediaStream
import org.jellyfin.playback.core.mediastream.PlayableMediaStream
@@ -21,22 +30,40 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.ZERO
import kotlin.time.Duration.Companion.milliseconds
@OptIn(UnstableApi::class)
class ExoPlayerBackend(
private val context: Context,
) : BasePlayerBackend() {
companion object {
const val TS_SEARCH_BYTES_LM = TsExtractor.TS_PACKET_SIZE * 1800
const val TS_SEARCH_BYTES_HM = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES
}
private var currentStream: PlayableMediaStream? = null
private val exoPlayer by lazy {
val renderersFactory = DefaultRenderersFactory(context).apply {
setEnableDecoderFallback(true)
setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
}
ExoPlayer.Builder(context, renderersFactory)
ExoPlayer.Builder(context)
.setRenderersFactory(DefaultRenderersFactory(context).apply {
setEnableDecoderFallback(true)
setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
})
.setTrackSelector(DefaultTrackSelector(context).apply {
setParameters(buildUponParameters().apply {
setAudioOffloadPreferences(TrackSelectionParameters.AudioOffloadPreferences.DEFAULT.buildUpon().apply {
setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
}.build())
})
})
.setMediaSourceFactory(DefaultMediaSourceFactory(
context,
DefaultExtractorsFactory().apply {
val isLowRamDevice = context.getSystemService<ActivityManager>()?.isLowRamDevice == true
setTsExtractorTimestampSearchBytes(when (isLowRamDevice) {
true -> TS_SEARCH_BYTES_LM
false -> TS_SEARCH_BYTES_HM
})
}
))
.setPauseAtEndOfMediaItems(true)
.build()
.also { player -> player.addListener(PlayerListener()) }

View File

@@ -1,6 +1,6 @@
package org.jellyfin.playback.exoplayer.mapping
import com.google.android.exoplayer2.util.MimeTypes
import androidx.media3.common.MimeTypes
fun getFfmpegAudioMimeType(codec: String): String {
return ffmpegAudioMimeTypes.getOrDefault(codec, codec)

View File

@@ -1,6 +1,8 @@
package org.jellyfin.playback.exoplayer.mapping
import com.google.android.exoplayer2.util.MimeTypes
import androidx.annotation.OptIn
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
fun getFfmpegContainerMimeType(codec: String): String {
// Find in container mime type list
@@ -13,6 +15,7 @@ fun getFfmpegContainerMimeType(codec: String): String {
}
}
@OptIn(UnstableApi::class)
val ffmpegContainerMimeTypes = mapOf(
"aac" to MimeTypes.AUDIO_AAC,
"alaw" to MimeTypes.AUDIO_ALAW,

View File

@@ -1,6 +1,8 @@
package org.jellyfin.playback.exoplayer.support
import com.google.android.exoplayer2.RendererCapabilities
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.RendererCapabilities
enum class AdaptiveSupport {
SEAMLESS,
@@ -8,6 +10,7 @@ enum class AdaptiveSupport {
NOT_SUPPORTED;
companion object {
@OptIn(UnstableApi::class)
fun fromFlags(flags: Int) = when (RendererCapabilities.getAdaptiveSupport(flags)) {
RendererCapabilities.ADAPTIVE_SEAMLESS -> SEAMLESS
RendererCapabilities.ADAPTIVE_NOT_SEAMLESS -> NOT_SEAMLESS

View File

@@ -1,6 +1,8 @@
package org.jellyfin.playback.exoplayer.support
import com.google.android.exoplayer2.RendererCapabilities
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.RendererCapabilities
enum class DecoderSupport {
PRIMARY,
@@ -8,6 +10,7 @@ enum class DecoderSupport {
FALLBACK;
companion object {
@OptIn(UnstableApi::class)
fun fromFlags(flags: Int) = when (RendererCapabilities.getDecoderSupport(flags)) {
RendererCapabilities.DECODER_SUPPORT_PRIMARY -> PRIMARY
RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE -> FALLBACK_MIMETYPE

View File

@@ -1,9 +1,11 @@
package org.jellyfin.playback.exoplayer.support
import com.google.android.exoplayer2.BaseRenderer
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.RendererCapabilities
import androidx.annotation.OptIn
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.BaseRenderer
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RendererCapabilities
import org.jellyfin.playback.core.support.PlaySupportReport
data class ExoPlayerPlaySupportReport(
@@ -16,6 +18,7 @@ data class ExoPlayerPlaySupportReport(
override val canPlay = format == FormatSupport.HANDLED || tunneling == true || hardwareAcceleration == true
companion object {
@OptIn(UnstableApi::class)
fun fromFlags(flags: Int): ExoPlayerPlaySupportReport = ExoPlayerPlaySupportReport(
format = FormatSupport.fromFlags(RendererCapabilities.getFormatSupport(flags)),
adaptive = AdaptiveSupport.fromFlags(RendererCapabilities.getAdaptiveSupport(flags)),
@@ -24,12 +27,14 @@ data class ExoPlayerPlaySupportReport(
decoder = DecoderSupport.fromFlags(RendererCapabilities.getDecoderSupport(flags)),
)
@OptIn(UnstableApi::class)
private fun tunnelingFromFlags(flags: Int) = when (RendererCapabilities.getTunnelingSupport(flags)) {
RendererCapabilities.TUNNELING_SUPPORTED -> true
RendererCapabilities.TUNNELING_NOT_SUPPORTED -> false
else -> null
}
@OptIn(UnstableApi::class)
private fun hardwareAccelerationFromFlags(flags: Int) = when (RendererCapabilities.getHardwareAccelerationSupport(flags)) {
RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED -> true
RendererCapabilities.HARDWARE_ACCELERATION_NOT_SUPPORTED -> false
@@ -41,6 +46,7 @@ data class ExoPlayerPlaySupportReport(
fun ExoPlayer.getPlaySupportReport(format: Format): ExoPlayerPlaySupportReport =
ExoPlayerPlaySupportReport.fromFlags(supportsFormat(format))
@OptIn(UnstableApi::class)
fun ExoPlayer.supportsFormat(format: Format): Int {
var capabilities = 0

View File

@@ -1,6 +1,9 @@
package org.jellyfin.playback.exoplayer.support
import com.google.android.exoplayer2.RendererCapabilities
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.RendererCapabilities
enum class FormatSupport {
HANDLED,
@@ -10,12 +13,13 @@ enum class FormatSupport {
UNSUPPORTED_TYPE;
companion object {
@OptIn(UnstableApi::class)
fun fromFlags(flags: Int) = when (RendererCapabilities.getFormatSupport(flags)) {
RendererCapabilities.FORMAT_HANDLED -> HANDLED
RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES -> EXCEEDS_CAPABILITIES
RendererCapabilities.FORMAT_UNSUPPORTED_DRM -> UNSUPPORTED_DRM
RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE -> UNSUPPORTED_SUBTYPE
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE -> UNSUPPORTED_TYPE
C.FORMAT_HANDLED -> HANDLED
C.FORMAT_EXCEEDS_CAPABILITIES -> EXCEEDS_CAPABILITIES
C.FORMAT_UNSUPPORTED_DRM -> UNSUPPORTED_DRM
C.FORMAT_UNSUPPORTED_SUBTYPE -> UNSUPPORTED_SUBTYPE
C.FORMAT_UNSUPPORTED_TYPE -> UNSUPPORTED_TYPE
else -> null
}
}

View File

@@ -1,11 +1,14 @@
package org.jellyfin.playback.exoplayer.support
import com.google.android.exoplayer2.Format
import androidx.annotation.OptIn
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import org.jellyfin.playback.core.mediastream.MediaStream
import org.jellyfin.playback.core.mediastream.MediaStreamAudioTrack
import org.jellyfin.playback.exoplayer.mapping.getFfmpegAudioMimeType
import org.jellyfin.playback.exoplayer.mapping.getFfmpegContainerMimeType
@OptIn(UnstableApi::class)
fun MediaStream.toFormat() = Format.Builder().also { f ->
f.setId(identifier)
f.setContainerMimeType(getFfmpegContainerMimeType(container.format))

View File

@@ -21,7 +21,8 @@ fun MediaStream.getMediaStreamTrack() = when (type) {
// Ignore other track types
MediaStreamType.EMBEDDED_IMAGE,
MediaStreamType.DATA -> null
MediaStreamType.DATA,
MediaStreamType.LYRIC -> null
}
private fun getAudioTrack(stream: MediaStream) = MediaStreamAudioTrack(