Merge branch 'cmp-rewrite' into simkl

This commit is contained in:
tapframe 2026-07-27 22:00:21 +05:30
commit db64bada04
18 changed files with 326 additions and 19 deletions

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"
tools:node="remove" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
tools:node="remove" />
<application>
<service
android:name="com.nuvio.app.features.player.PlayerNowPlayingService"
tools:node="remove" />
</application>
</manifest>

View file

@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
actual val heroTrailerPlaybackSupported: Boolean = true
actual val inAppUpdaterEnabled: Boolean = true
actual val imdbRatingLogoEnabled: Boolean = true
actual val mediaPlaybackForegroundServiceEnabled: Boolean = true
}

View file

@ -0,0 +1,19 @@
package com.nuvio.app.core.auth
import android.os.Build
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
val deviceName = formatDeviceName(
manufacturer = Build.MANUFACTURER.orEmpty(),
model = Build.MODEL.orEmpty(),
fallback = "Android device",
)
val osVersion = Build.VERSION.RELEASE.orEmpty()
.trim()
.ifBlank { Build.VERSION.SDK_INT.toString() }
return DeviceClientMetadata(
deviceName = deviceName,
platform = "Android $osVersion",
)
}

View file

@ -7,6 +7,7 @@ import android.text.SpannableString
import android.net.Uri
import android.util.Log
import android.util.TypedValue
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import android.os.SystemClock
@ -39,6 +40,7 @@ import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.VideoSize
import androidx.media3.common.text.Cue
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
@ -254,6 +256,9 @@ private fun ExoPlayerSurface(
var selectedExternalSubtitleMimeType by remember(playerSourceKey) { mutableStateOf<String?>(null) }
val latestSubtitleDelayMs = rememberUpdatedState(subtitleDelayMs)
val latestExternalSubtitleMimeType = rememberUpdatedState(selectedExternalSubtitleMimeType)
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
var videoAspectRatio by remember(playerSourceKey) { mutableStateOf(0f) }
val latestVideoAspectRatio = rememberUpdatedState(videoAspectRatio)
var decoderPriorityOverride by remember(playerSourceKey) { mutableStateOf<Int?>(null) }
var fallbackStartPositionMs by remember(playerSourceKey) { mutableStateOf<Long?>(null) }
val effectiveDecoderPriority = decoderPriorityOverride ?: playerSettings.decoderPriority
@ -342,6 +347,9 @@ private fun ExoPlayerSurface(
shouldNormalizeCuePositionProvider = {
latestExternalSubtitleMimeType.value == MimeTypes.TEXT_VTT
},
videoBoundsFractionProvider = {
playerViewRef?.videoBoundsFraction(latestVideoAspectRatio.value)
},
)
.setExtensionRendererMode(effectiveDecoderPriority)
.setEnableDecoderFallback(true)
@ -449,7 +457,6 @@ private fun ExoPlayerSurface(
val pendingSubtitleTrackIndex = remember { mutableListOf<Int>() }
val pendingAudioTrackSelection = remember { mutableListOf<TrackSelectionSnapshot>() }
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
val isInPip = rememberIsInPictureInPicture()
@ -580,10 +587,6 @@ private fun ExoPlayerSurface(
dispatchExoPlayerSnapshot()
}
override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {
latestOnSnapshot.value(exoPlayer.snapshot())
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.i(
PLAYER_DIAGNOSTIC_TAG,
@ -608,6 +611,13 @@ private fun ExoPlayerSurface(
dispatchExoPlayerSnapshot()
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
latestOnSnapshot.value(exoPlayer.snapshot())
if (videoSize.width > 0 && videoSize.height > 0) {
videoAspectRatio = videoSize.width.toFloat() / videoSize.height.toFloat()
}
}
override fun onTracksChanged(tracks: androidx.media3.common.Tracks) {
Log.d(TAG, "onTracksChanged: ${tracks.groups.size} groups total")
exoPlayer.logCurrentTracks("onTracksChanged")
@ -1794,11 +1804,50 @@ private fun ExoPlayer.logCurrentTracks(context: String) {
Log.d(TAG, "--- end logCurrentTracks ---")
}
@androidx.annotation.OptIn(UnstableApi::class)
private fun PlayerView.videoBoundsFraction(aspectRatio: Float): RectF? {
val subtitleView = this.subtitleView ?: return null
val viewWidth = subtitleView.width.toFloat()
val viewHeight = subtitleView.height.toFloat()
if (viewWidth <= 0f || viewHeight <= 0f) return null
if (aspectRatio > 0f) {
val parentRatio = viewWidth / viewHeight
return if (parentRatio > aspectRatio) {
val fitW = viewHeight * aspectRatio
val leftPx = (viewWidth - fitW) / 2f
RectF(leftPx / viewWidth, 0f, (leftPx + fitW) / viewWidth, 1f)
} else {
val fitH = viewWidth / aspectRatio
val topPx = (viewHeight - fitH) / 2f
RectF(0f, topPx / viewHeight, 1f, (topPx + fitH) / viewHeight)
}
}
val contentFrame = getTag(androidx.media3.ui.R.id.exo_content_frame) as? AspectRatioFrameLayout
?: findViewById<AspectRatioFrameLayout>(androidx.media3.ui.R.id.exo_content_frame)
?.also { setTag(androidx.media3.ui.R.id.exo_content_frame, it) }
?: return null
val frameWidth = contentFrame.width.toFloat()
val frameHeight = contentFrame.height.toFloat()
if (frameWidth <= 0f || frameHeight <= 0f) return null
if (frameWidth > viewWidth || frameHeight > viewHeight) return null
val left = contentFrame.x / viewWidth
val top = contentFrame.y / viewHeight
return RectF(
left,
top,
left + frameWidth / viewWidth,
top + frameHeight / viewHeight,
)
}
@androidx.annotation.OptIn(UnstableApi::class)
private class SubtitleOffsetRenderersFactory(
context: Context,
private val subtitleDelayUsProvider: () -> Long,
private val shouldNormalizeCuePositionProvider: () -> Boolean,
private val videoBoundsFractionProvider: () -> RectF?,
) : DefaultRenderersFactory(context) {
override fun buildTextRenderers(
context: Context,
@ -1810,6 +1859,7 @@ private class SubtitleOffsetRenderersFactory(
val normalizingOutput = CueNormalizingTextOutput(
delegate = output,
shouldNormalizeCuePositionProvider = shouldNormalizeCuePositionProvider,
videoBoundsFractionProvider = videoBoundsFractionProvider,
)
val startIndex = out.size
super.buildTextRenderers(context, normalizingOutput, outputLooper, extensionRendererMode, out)
@ -1825,6 +1875,7 @@ private class SubtitleOffsetRenderersFactory(
private class CueNormalizingTextOutput(
private val delegate: TextOutput,
private val shouldNormalizeCuePositionProvider: () -> Boolean,
private val videoBoundsFractionProvider: () -> RectF?,
) : TextOutput {
override fun onCues(cueGroup: CueGroup) {
val processed = cueGroup.cues.map(::processCue)
@ -1841,9 +1892,36 @@ private class CueNormalizingTextOutput(
if (shouldNormalizeCuePositionProvider()) {
processed = normalizeCuePosition(processed)
}
if (processed.bitmap != null) {
val bounds = videoBoundsFractionProvider()
if (bounds != null && bounds.width() > 0f && bounds.height() > 0f) {
val isIdentity = bounds.left == 0f && bounds.top == 0f
&& bounds.width() == 1f && bounds.height() == 1f
if (!isIdentity) {
processed = remapBitmapCueToVideoBounds(processed, bounds)
}
}
}
return processed
}
private fun remapBitmapCueToVideoBounds(cue: Cue, bounds: RectF): Cue {
val builder = cue.buildUpon()
if (cue.position != Cue.DIMEN_UNSET) {
builder.setPosition(bounds.left + cue.position * bounds.width())
}
if (cue.size != Cue.DIMEN_UNSET) {
builder.setSize(cue.size * bounds.width())
}
if (cue.lineType == Cue.LINE_TYPE_FRACTION && cue.line != Cue.DIMEN_UNSET) {
builder.setLine(bounds.top + cue.line * bounds.height(), Cue.LINE_TYPE_FRACTION)
}
if (cue.bitmapHeight != Cue.DIMEN_UNSET) {
builder.setBitmapHeight(cue.bitmapHeight * bounds.height())
}
return builder.build()
}
private fun normalizeCuePosition(cue: Cue): Cue {
if (cue.bitmap != null || cue.verticalType != Cue.TYPE_UNSET || cue.line == Cue.DIMEN_UNSET) {
return cue

View file

@ -20,6 +20,7 @@ import android.os.Looper
import android.os.SystemClock
import android.util.Log
import com.nuvio.app.R
import com.nuvio.app.core.build.AppFeaturePolicy
import java.io.ByteArrayOutputStream
import java.lang.ref.WeakReference
import java.net.HttpURLConnection
@ -106,7 +107,9 @@ internal class AndroidPlayerNowPlayingController(
get() = !released && metadata != null
init {
createNotificationChannel(appContext)
if (AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) {
createNotificationChannel(appContext)
}
AndroidNowPlayingActionDispatcher.register(this)
}
@ -288,6 +291,7 @@ internal class AndroidPlayerNowPlayingController(
}
private fun publishNotification() {
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
val currentMetadata = metadata ?: return
val notification = buildNotification(
context = appContext,
@ -377,6 +381,7 @@ class PlayerNowPlayingService : Service() {
companion object {
internal fun publish(context: Context, notification: Notification) {
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
PlayerNowPlayingServiceState.notification = notification
val intent = Intent(context, PlayerNowPlayingService::class.java)
.setAction(ACTION_START_FOREGROUND)
@ -394,6 +399,7 @@ class PlayerNowPlayingService : Service() {
}
internal fun hide(context: Context) {
if (!AppFeaturePolicy.mediaPlaybackForegroundServiceEnabled) return
PlayerNowPlayingServiceState.notification = null
runCatching { context.stopService(Intent(context, PlayerNowPlayingService::class.java)) }
context.getSystemService(NotificationManager::class.java)

View file

@ -3,11 +3,12 @@ package com.nuvio.app.core.build
actual object AppFeaturePolicy {
actual val pluginsEnabled: Boolean = false
actual val supportersContributorsPageEnabled: Boolean = true
actual val accountDeletionEnabled: Boolean = false
actual val accountDeletionEnabled: Boolean = true
actual val personalMediaAddonCopyEnabled: Boolean = false
actual val p2pEnabled: Boolean = true
actual val trailerPlaybackMode: TrailerPlaybackMode = TrailerPlaybackMode.EXTERNAL
actual val heroTrailerPlaybackSupported: Boolean = false
actual val inAppUpdaterEnabled: Boolean = false
actual val imdbRatingLogoEnabled: Boolean = false
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
}

View file

@ -434,6 +434,7 @@
<string name="compose_settings_page_playback">Playback</string>
<string name="compose_settings_page_plugins">Plugins</string>
<string name="compose_settings_page_poster_customization">Poster Card Style</string>
<string name="compose_settings_page_privacy_policy">Privacy Policy</string>
<string name="compose_settings_page_root">Settings</string>
<string name="compose_settings_page_streams">Streams</string>
<string name="settings_appearance_section_streams">STREAMS</string>
@ -457,6 +458,7 @@
<string name="compose_settings_root_general_section">GENERAL</string>
<string name="compose_settings_root_integrations_description">Manage available integrations</string>
<string name="compose_settings_root_notifications_description">Manage episode release alerts and send a test notification.</string>
<string name="compose_settings_root_privacy_policy_description">Learn how Nuvio handles your data</string>
<string name="compose_settings_root_streams_description">Stream result display and badge URL rules.</string>
<string name="compose_settings_root_switch_profile_description">Change to a different profile.</string>
<string name="compose_settings_root_switch_profile_title">Switch Profile</string>

View file

@ -86,6 +86,7 @@ import coil3.svg.SvgDecoder
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.auth.DeviceSessionRegistration
import com.nuvio.app.core.deeplink.AppDeepLink
import com.nuvio.app.core.deeplink.AppDeepLinkRepository
import com.nuvio.app.core.network.NetworkCondition
@ -498,6 +499,11 @@ fun App(
NetworkStatusRepository.uiState
}.collectAsStateWithLifecycle()
LaunchedEffect(authState) {
if (!ownsAppRuntime) return@LaunchedEffect
DeviceSessionRegistration.registerIfAuthenticated(force = true)
}
LaunchedEffect(
profileState.activeProfile?.profileIndex,
profileState.activeProfile?.name,
@ -1063,6 +1069,7 @@ private fun MainAppContent(
if (!ownsAppRuntime) return@LaunchedEffect
AppForegroundMonitor.events().collect {
NetworkStatusRepository.requestForegroundRefresh()
DeviceSessionRegistration.registerIfAuthenticated()
}
}

View file

@ -0,0 +1,90 @@
package com.nuvio.app.core.auth
import co.touchlab.kermit.Logger
import com.nuvio.app.core.build.AppVersionConfig
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.sync.SyncClientIdentity
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlin.time.Duration.Companion.minutes
import kotlin.time.TimeMark
import kotlin.time.TimeSource
private const val CLIENT_NAME = "Nuvio Mobile"
private val REGISTRATION_INTERVAL = 15.minutes
internal data class DeviceClientMetadata(
val deviceName: String,
val platform: String,
)
internal expect fun currentDeviceClientMetadata(): DeviceClientMetadata
object DeviceSessionRegistration {
private val log = Logger.withTag("DeviceSessionRegistration")
private val registrationMutex = Mutex()
private var lastRegistration: TimeMark? = null
suspend fun registerIfAuthenticated(force: Boolean = false): Boolean {
val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return false
if (authState.isAnonymous) return false
return registrationMutex.withLock {
if (!force && lastRegistration?.elapsedNow()?.let { it < REGISTRATION_INTERVAL } == true) {
return@withLock true
}
val metadata = currentDeviceClientMetadata()
val params = buildDeviceRegistrationParams(
installationId = SyncClientIdentity.currentClientId(),
clientVersion = AppVersionConfig.VERSION_NAME,
metadata = metadata,
)
try {
SupabaseProvider.client.postgrest.rpc("register_current_device", params)
lastRegistration = TimeSource.Monotonic.markNow()
true
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
log.w(error) { "Device session registration failed" }
false
}
}
}
}
internal fun buildDeviceRegistrationParams(
installationId: String,
clientVersion: String,
metadata: DeviceClientMetadata,
): JsonObject = buildJsonObject {
put("p_installation_id", installationId)
put("p_client_name", CLIENT_NAME)
put("p_client_version", clientVersion)
put("p_platform", metadata.platform)
put("p_device_name", metadata.deviceName)
}
internal fun formatDeviceName(
manufacturer: String,
model: String,
fallback: String,
): String {
val normalizedManufacturer = manufacturer.trim()
val normalizedModel = model.trim()
return when {
normalizedModel.isBlank() -> fallback
normalizedManufacturer.isBlank() -> normalizedModel
normalizedModel.startsWith(normalizedManufacturer, ignoreCase = true) -> normalizedModel
else -> "$normalizedManufacturer $normalizedModel"
}
}

View file

@ -15,4 +15,5 @@ expect object AppFeaturePolicy {
val heroTrailerPlaybackSupported: Boolean
val inAppUpdaterEnabled: Boolean
val imdbRatingLogoEnabled: Boolean
val mediaPlaybackForegroundServiceEnabled: Boolean
}

View file

@ -12,6 +12,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.drawscope.clipPath
@Composable
fun rememberCardDepthStyleUiState(): CardDepthStyleUiState {
CardDepthStyleRepository.ensureLoaded()
@ -77,20 +79,31 @@ fun Modifier.cardDepthVisual(
drawContent()
val sheenHeight = size.height * 0.22f
if (sheenHeight > 0f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = sheen),
Color.Transparent,
val outline = shape.createOutline(size, layoutDirection, this)
val shapePath = outline.toPath()
clipPath(shapePath) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = sheen),
Color.Transparent,
),
startY = 0f,
endY = sheenHeight,
),
startY = 0f,
endY = sheenHeight,
),
size = Size(size.width, sheenHeight),
)
size = Size(size.width, sheenHeight),
)
}
}
}
} else {
withEdge
}
}
private fun androidx.compose.ui.graphics.Outline.toPath(): androidx.compose.ui.graphics.Path = when (this) {
is androidx.compose.ui.graphics.Outline.Rectangle -> androidx.compose.ui.graphics.Path().apply { addRect(rect) }
is androidx.compose.ui.graphics.Outline.Rounded -> androidx.compose.ui.graphics.Path().apply { addRoundRect(roundRect) }
is androidx.compose.ui.graphics.Outline.Generic -> path
}

View file

@ -16,10 +16,12 @@ import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.People
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Policy
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.build.AppVersionConfig
@ -33,6 +35,7 @@ import nuvio.composeapp.generated.resources.compose_settings_page_integrations
import nuvio.composeapp.generated.resources.compose_settings_page_licenses_attributions
import nuvio.composeapp.generated.resources.compose_settings_page_notifications
import nuvio.composeapp.generated.resources.compose_settings_page_playback
import nuvio.composeapp.generated.resources.compose_settings_page_privacy_policy
import nuvio.composeapp.generated.resources.compose_settings_page_supporters_contributors
import nuvio.composeapp.generated.resources.compose_settings_root_account_description
import nuvio.composeapp.generated.resources.compose_settings_root_appearance_description
@ -44,6 +47,7 @@ import nuvio.composeapp.generated.resources.compose_settings_root_downloads_titl
import nuvio.composeapp.generated.resources.compose_settings_root_general_section
import nuvio.composeapp.generated.resources.compose_settings_root_integrations_description
import nuvio.composeapp.generated.resources.compose_settings_root_notifications_description
import nuvio.composeapp.generated.resources.compose_settings_root_privacy_policy_description
import nuvio.composeapp.generated.resources.compose_settings_root_switch_profile_description
import nuvio.composeapp.generated.resources.compose_settings_root_switch_profile_title
import nuvio.composeapp.generated.resources.compose_settings_root_tracking_description
@ -60,6 +64,8 @@ import nuvio.composeapp.generated.resources.about_supporters_contributors_subtit
import nuvio.composeapp.generated.resources.about_licenses_attributions_subtitle
import org.jetbrains.compose.resources.stringResource
private const val PRIVACY_POLICY_URL = "https://nuvio.tv/privacy-policy"
internal fun LazyListScope.settingsRootContent(
isTablet: Boolean,
onPlaybackClick: () -> Unit,
@ -178,6 +184,7 @@ internal fun LazyListScope.settingsRootContent(
}
if (showAboutSection) {
item {
val uriHandler = LocalUriHandler.current
SettingsSection(
title = stringResource(Res.string.compose_settings_root_about_section),
isTablet = isTablet,
@ -193,6 +200,14 @@ internal fun LazyListScope.settingsRootContent(
)
SettingsGroupDivider(isTablet = isTablet)
}
SettingsNavigationRow(
title = stringResource(Res.string.compose_settings_page_privacy_policy),
description = stringResource(Res.string.compose_settings_root_privacy_policy_description),
icon = Icons.Rounded.Policy,
isTablet = isTablet,
onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) },
)
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = stringResource(Res.string.compose_settings_page_licenses_attributions),
description = stringResource(Res.string.about_licenses_attributions_subtitle),

View file

@ -0,0 +1,36 @@
package com.nuvio.app.core.auth
import kotlin.test.Test
import kotlin.test.assertEquals
class DeviceSessionRegistrationTest {
@Test
fun buildsOfficialClientRegistrationPayload() {
val params = buildDeviceRegistrationParams(
installationId = "nuvio-mobile-installation",
clientVersion = "1.2.3",
metadata = DeviceClientMetadata(
deviceName = "Google Pixel 9",
platform = "Android 16",
),
)
assertEquals("nuvio-mobile-installation", params.getValue("p_installation_id").toString().trim('"'))
assertEquals("Nuvio Mobile", params.getValue("p_client_name").toString().trim('"'))
assertEquals("1.2.3", params.getValue("p_client_version").toString().trim('"'))
assertEquals("Android 16", params.getValue("p_platform").toString().trim('"'))
assertEquals("Google Pixel 9", params.getValue("p_device_name").toString().trim('"'))
}
@Test
fun combinesManufacturerAndModelWithoutRepeatingManufacturer() {
assertEquals(
"Samsung SM-S918B",
formatDeviceName("Samsung", "SM-S918B", "Android device"),
)
assertEquals(
"Google Pixel 9",
formatDeviceName("Google", "Google Pixel 9", "Android device"),
)
}
}

View file

@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
actual val heroTrailerPlaybackSupported: Boolean = false
actual val inAppUpdaterEnabled: Boolean = false
actual val imdbRatingLogoEnabled: Boolean = true
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
}

View file

@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
actual val heroTrailerPlaybackSupported: Boolean = false
actual val inAppUpdaterEnabled: Boolean = false
actual val imdbRatingLogoEnabled: Boolean = false
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
}

View file

@ -10,4 +10,5 @@ actual object AppFeaturePolicy {
actual val heroTrailerPlaybackSupported: Boolean = false
actual val inAppUpdaterEnabled: Boolean = false
actual val imdbRatingLogoEnabled: Boolean = true
actual val mediaPlaybackForegroundServiceEnabled: Boolean = false
}

View file

@ -0,0 +1,17 @@
package com.nuvio.app.core.auth
import platform.UIKit.UIDevice
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
val device = UIDevice.currentDevice
val deviceName = device.name
.trim()
.ifBlank { device.localizedModel.trim() }
.ifBlank { "Apple device" }
val platform = "${device.systemName()} ${device.systemVersion}".trim()
return DeviceClientMetadata(
deviceName = deviceName,
platform = platform,
)
}

View file

@ -1,3 +1,3 @@
CURRENT_PROJECT_VERSION=100
MARKETING_VERSION=0.3.1
CURRENT_PROJECT_VERSION=101
MARKETING_VERSION=0.3.2