Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a257651f8e | ||
|
|
0690c1c898 | ||
|
|
66ec39f566 | ||
|
|
f4abd1246d | ||
|
|
a17821181f | ||
|
|
4da68f86cb | ||
|
|
487bf3b7ee | ||
|
|
21f5256e0f | ||
|
|
41df9a59df | ||
|
|
3709bf5f5a | ||
|
|
3cdc1cdaba | ||
|
|
6031979f4c | ||
|
|
896f66254f | ||
|
|
3a18933b87 | ||
|
|
8c45084406 | ||
|
|
d4a297710b | ||
|
|
19f30c19b0 | ||
|
|
4e8e8bc232 | ||
|
|
ec5c751390 | ||
|
|
46b0b7aade | ||
|
|
3877739320 | ||
|
|
538eeb4f22 | ||
|
|
0d724c532a | ||
|
|
eba14e0bf4 | ||
|
|
a8c35b0ea0 | ||
|
|
17744729b2 | ||
|
|
fd5442097e | ||
|
|
f9eeae6bf0 | ||
|
|
c2e33b9b13 | ||
|
|
9c4b121574 | ||
|
|
ddb1be18bc | ||
|
|
4a4c1ed6ca | ||
|
|
c28759e165 | ||
|
|
45a5adbc0b | ||
|
|
118eacfa90 | ||
|
|
91ccf32f77 | ||
|
|
065e738773 | ||
|
|
65a7bbf501 | ||
|
|
d661c2faf3 | ||
|
|
8175dbd1c0 | ||
|
|
f068be6945 | ||
|
|
fe465db020 | ||
|
|
d6fe265b38 | ||
|
|
8eecfab5d7 | ||
|
|
0f9ffcacb9 | ||
|
|
aa7c45d462 | ||
|
|
156f5ecd3c | ||
|
|
e56fd0d024 | ||
|
|
06e245fe48 | ||
|
|
9f0fa644a7 | ||
|
|
5b213199dd | ||
|
|
b4f6273ede | ||
|
|
66c624e16c | ||
|
|
e47916f1c2 | ||
|
|
ab7875cbb3 | ||
|
|
5f15615a96 | ||
|
|
9cda86ab84 | ||
|
|
1873497fec |
30
.github/workflows/app-publish.yaml
vendored
@@ -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 .;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -203,7 +203,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
|
||||
}
|
||||
};
|
||||
|
||||
backgroundService.getValue().clearBackgrounds();
|
||||
backgroundService.getValue().disable();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, " ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval"
|
||||
android:tint="@color/logo_background"></shape>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval"
|
||||
android:tint="#000B25"></shape>
|
||||
@@ -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>
|
||||
|
||||
13
app/src/main/res/drawable/app_icon_foreground_monochrome.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
BIN
app/src/main/res/mipmap-hdpi/app_banner.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
app/src/main/res/mipmap-hdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/app_banner.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/mipmap-mdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/app_banner.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/app_banner.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/app_banner.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 51 KiB |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||