mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 15:49:30 +00:00
Send resolved intro/outro skip segments to external players
Adds an opt-in External Player setting that hands resolved intro and outro skip timestamps to external players via a skip_segments intent extra, so a supporting player can auto-skip the opening and ending the same way the internal player already does. Resolution reuses the existing SkipIntroRepository (IntroDB, ARM, AniSkip, Anime-Skip), so there is no new API, key, provider, or dependency. The toggle is off by default and sits under Forward Subtitles in the External Player settings. Resolution is bounded by a timeout and only runs when the setting is on; players that do not read the extra ignore it.
This commit is contained in:
parent
21a9e2df69
commit
af2e8ff926
11 changed files with 159 additions and 12 deletions
|
|
@ -93,6 +93,10 @@ internal actual object ExternalPlayerPlatform {
|
|||
// Required by MX Player; harmless for other players.
|
||||
putExtra("return_result", true)
|
||||
|
||||
// Intro/outro skip segments for players that support auto-skipping.
|
||||
// Players that don't understand this extra simply ignore it.
|
||||
request.skipSegmentsJson?.let { putExtra("skip_segments", it) }
|
||||
|
||||
// Headers
|
||||
if (request.sourceHeaders.isNotEmpty()) {
|
||||
val headerArray = request.sourceHeaders.entries
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ actual object PlayerSettingsStorage {
|
|||
private const val touchGesturesEnabledKey = "touch_gestures_enabled"
|
||||
private const val externalPlayerEnabledKey = "external_player_enabled"
|
||||
private const val externalPlayerForwardSubtitlesKey = "external_player_forward_subtitles"
|
||||
private const val externalPlayerSendSkipSegmentsKey = "external_player_send_skip_segments"
|
||||
private const val externalPlayerIdKey = "external_player_id"
|
||||
private const val preferredAudioLanguageKey = "preferred_audio_language"
|
||||
private const val secondaryPreferredAudioLanguageKey = "secondary_preferred_audio_language"
|
||||
|
|
@ -93,6 +94,7 @@ actual object PlayerSettingsStorage {
|
|||
touchGesturesEnabledKey,
|
||||
externalPlayerEnabledKey,
|
||||
externalPlayerForwardSubtitlesKey,
|
||||
externalPlayerSendSkipSegmentsKey,
|
||||
externalPlayerIdKey,
|
||||
preferredAudioLanguageKey,
|
||||
secondaryPreferredAudioLanguageKey,
|
||||
|
|
@ -270,6 +272,23 @@ actual object PlayerSettingsStorage {
|
|||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadExternalPlayerSendSkipSegments(): Boolean? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val key = ProfileScopedKey.of(externalPlayerSendSkipSegmentsKey)
|
||||
if (sharedPreferences.contains(key)) {
|
||||
sharedPreferences.getBoolean(key, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveExternalPlayerSendSkipSegments(enabled: Boolean) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putBoolean(ProfileScopedKey.of(externalPlayerSendSkipSegmentsKey), enabled)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadExternalPlayerId(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(externalPlayerIdKey), null)
|
||||
|
||||
|
|
@ -1042,6 +1061,7 @@ actual object PlayerSettingsStorage {
|
|||
loadTouchGesturesEnabled()?.let { put(touchGesturesEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerEnabled()?.let { put(externalPlayerEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerForwardSubtitles()?.let { put(externalPlayerForwardSubtitlesKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerSendSkipSegments()?.let { put(externalPlayerSendSkipSegmentsKey, encodeSyncBoolean(it)) }
|
||||
loadExternalPlayerId()?.let { put(externalPlayerIdKey, encodeSyncString(it)) }
|
||||
loadPreferredAudioLanguage()?.let { put(preferredAudioLanguageKey, encodeSyncString(it)) }
|
||||
loadSecondaryPreferredAudioLanguage()?.let { put(secondaryPreferredAudioLanguageKey, encodeSyncString(it)) }
|
||||
|
|
@ -1115,6 +1135,7 @@ actual object PlayerSettingsStorage {
|
|||
payload.decodeSyncBoolean(touchGesturesEnabledKey)?.let(::saveTouchGesturesEnabled)
|
||||
payload.decodeSyncBoolean(externalPlayerEnabledKey)?.let(::saveExternalPlayerEnabled)
|
||||
payload.decodeSyncBoolean(externalPlayerForwardSubtitlesKey)?.let(::saveExternalPlayerForwardSubtitles)
|
||||
payload.decodeSyncBoolean(externalPlayerSendSkipSegmentsKey)?.let(::saveExternalPlayerSendSkipSegments)
|
||||
payload.decodeSyncString(externalPlayerIdKey)?.let(::saveExternalPlayerId)
|
||||
payload.decodeSyncString(preferredAudioLanguageKey)?.let(::savePreferredAudioLanguage)
|
||||
payload.decodeSyncString(secondaryPreferredAudioLanguageKey)?.let(::saveSecondaryPreferredAudioLanguage)
|
||||
|
|
|
|||
|
|
@ -855,6 +855,8 @@
|
|||
<string name="settings_playback_external_player_description_ios">Open new playback with the selected installed player.</string>
|
||||
<string name="settings_playback_external_player_forward_subtitles">Forward Subtitles to External Player</string>
|
||||
<string name="settings_playback_external_player_forward_subtitles_description">Fetch addon subtitles in your preferred language and pass them to the external player.</string>
|
||||
<string name="settings_playback_external_player_send_skip_segments">Send Intro and Outro Timestamps</string>
|
||||
<string name="settings_playback_external_player_send_skip_segments_description">Pass resolved intro and outro skip timestamps to the external player for auto-skipping. Only works in players that support it; other players ignore it.</string>
|
||||
<string name="settings_playback_external_player_none_available">No supported external players installed</string>
|
||||
<string name="settings_playback_player_preference">Player</string>
|
||||
<string name="settings_playback_player_preference_internal">Internal</string>
|
||||
|
|
@ -1317,6 +1319,7 @@
|
|||
<string name="streams_fetching">Fetching…</string>
|
||||
<string name="streams_finding_source">Finding source…</string>
|
||||
<string name="streams_loading_subtitles">Loading subtitles from addons…</string>
|
||||
<string name="streams_loading_skip_segments">Loading skip segments…</string>
|
||||
<string name="streams_finding_streams">Finding streams…</string>
|
||||
<string name="streams_link_copied">Stream link copied</string>
|
||||
<string name="streams_no_direct_link">No direct stream link available</string>
|
||||
|
|
|
|||
|
|
@ -1082,14 +1082,18 @@ private fun MainAppContent(
|
|||
val baseRequest = launch.toExternalPlayerPlaybackRequest()
|
||||
val shouldForwardSubtitles = playerSettingsUiState.externalPlayerForwardSubtitles &&
|
||||
!playerSettingsUiState.preferredSubtitleLanguage.equals(SubtitleLanguageOption.NONE, ignoreCase = true)
|
||||
val shouldSendSkipSegments = playerSettingsUiState.externalPlayerSendSkipSegments
|
||||
if (shouldForwardSubtitles) {
|
||||
StreamsRepository.setOverlayVisible(true, getString(Res.string.streams_loading_subtitles))
|
||||
} else if (shouldSendSkipSegments) {
|
||||
StreamsRepository.setOverlayVisible(true, getString(Res.string.streams_loading_skip_segments))
|
||||
}
|
||||
val enrichedRequest = prepareExternalPlayerLaunch(
|
||||
request = baseRequest,
|
||||
type = launch.contentType ?: launch.parentMetaType,
|
||||
videoId = launch.videoId ?: launch.parentMetaId,
|
||||
forwardSubtitles = playerSettingsUiState.externalPlayerForwardSubtitles,
|
||||
sendSkipSegments = shouldSendSkipSegments,
|
||||
preferredLanguage = playerSettingsUiState.preferredSubtitleLanguage,
|
||||
secondaryLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage,
|
||||
onOverlayMessage = { _ -> },
|
||||
|
|
|
|||
|
|
@ -1,19 +1,32 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import com.nuvio.app.features.player.skip.SkipInterval
|
||||
import com.nuvio.app.features.player.skip.SkipIntroRepository
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.addJsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
private const val SkipSegmentResolveTimeoutMs = 4_000L
|
||||
|
||||
/**
|
||||
* Orchestrates the full external player launch flow:
|
||||
* fetches subtitles if forwarding is enabled, downloads them to local cache,
|
||||
* then returns an enriched request for the caller to dispatch.
|
||||
* resolves intro/outro skip segments if enabled, then returns an enriched
|
||||
* request for the caller to dispatch.
|
||||
*/
|
||||
suspend fun prepareExternalPlayerLaunch(
|
||||
request: ExternalPlayerPlaybackRequest,
|
||||
type: String,
|
||||
videoId: String,
|
||||
forwardSubtitles: Boolean,
|
||||
sendSkipSegments: Boolean,
|
||||
preferredLanguage: String,
|
||||
secondaryLanguage: String?,
|
||||
onOverlayMessage: (String?) -> Unit,
|
||||
): ExternalPlayerPlaybackRequest {
|
||||
var result = request
|
||||
|
||||
if (forwardSubtitles && !preferredLanguage.equals(SubtitleLanguageOption.NONE, ignoreCase = true)) {
|
||||
onOverlayMessage("Loading subtitles from addons...")
|
||||
|
||||
|
|
@ -27,13 +40,61 @@ suspend fun prepareExternalPlayerLaunch(
|
|||
if (subtitles != null) {
|
||||
onOverlayMessage("Downloading subtitles...")
|
||||
val cachedSubtitles = SubtitleCacheProvider.cacheForExternalPlayer(subtitles)
|
||||
if (cachedSubtitles != null) {
|
||||
return request.copy(subtitles = cachedSubtitles)
|
||||
}
|
||||
// Fallback: use original URLs if caching fails
|
||||
return request.copy(subtitles = subtitles)
|
||||
result = result.copy(subtitles = cachedSubtitles ?: subtitles)
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
if (sendSkipSegments) {
|
||||
val skipSegmentsJson = resolveSkipSegmentsJson(videoId, request.season, request.episode)
|
||||
if (skipSegmentsJson != null) {
|
||||
result = result.copy(skipSegmentsJson = skipSegmentsJson)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves intro/outro skip segments for the given content and serializes them to the
|
||||
* JSON contract understood by supporting external players: a JSON array of objects with
|
||||
* `type` (String), `start` (seconds) and `end` (seconds). Returns null if nothing resolved.
|
||||
*
|
||||
* Bounded by a timeout so a slow provider never delays the player launch. Resolution is
|
||||
* intentionally independent of the in-app skip-intro toggle (requireSkipIntroEnabled = false):
|
||||
* this is its own opt-in setting.
|
||||
*/
|
||||
private suspend fun resolveSkipSegmentsJson(videoId: String, season: Int?, episode: Int?): String? {
|
||||
val ep = episode ?: return null
|
||||
val intervals = withTimeoutOrNull(SkipSegmentResolveTimeoutMs) {
|
||||
when {
|
||||
videoId.startsWith("mal:") -> {
|
||||
val malId = videoId.removePrefix("mal:").substringBefore(':')
|
||||
SkipIntroRepository.getSkipIntervalsForMal(malId, ep, requireSkipIntroEnabled = false)
|
||||
}
|
||||
videoId.startsWith("kitsu:") -> {
|
||||
val kitsuId = videoId.removePrefix("kitsu:").substringBefore(':')
|
||||
SkipIntroRepository.getSkipIntervalsForKitsu(kitsuId, ep, requireSkipIntroEnabled = false)
|
||||
}
|
||||
else -> {
|
||||
val imdbId = videoId.substringBefore(':').takeIf { it.startsWith("tt") } ?: return@withTimeoutOrNull null
|
||||
val s = season ?: return@withTimeoutOrNull null
|
||||
SkipIntroRepository.getSkipIntervals(imdbId, s, ep, requireSkipIntroEnabled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intervals.isNullOrEmpty()) return null
|
||||
return intervals.toSkipSegmentsJson()
|
||||
}
|
||||
|
||||
private fun List<SkipInterval>.toSkipSegmentsJson(): String =
|
||||
buildJsonArray {
|
||||
forEach { interval ->
|
||||
addJsonObject {
|
||||
put("type", interval.type)
|
||||
put("start", interval.startTime)
|
||||
put("end", interval.endTime)
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ data class ExternalPlayerPlaybackRequest(
|
|||
val season: Int? = null,
|
||||
val episode: Int? = null,
|
||||
val episodeTitle: String? = null,
|
||||
// JSON array of intro/outro skip segments, passed to players that support auto-skipping.
|
||||
val skipSegmentsJson: String? = null,
|
||||
) {
|
||||
/**
|
||||
* Builds a display title for external players.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ data class PlayerSettingsUiState(
|
|||
val touchGesturesEnabled: Boolean = true,
|
||||
val externalPlayerEnabled: Boolean = false,
|
||||
val externalPlayerForwardSubtitles: Boolean = false,
|
||||
val externalPlayerSendSkipSegments: Boolean = false,
|
||||
val externalPlayerId: String? = ExternalPlayerPlatform.defaultPlayerId(),
|
||||
val preferredAudioLanguage: String = AudioLanguageOption.DEVICE,
|
||||
val secondaryPreferredAudioLanguage: String? = null,
|
||||
|
|
@ -103,6 +104,7 @@ object PlayerSettingsRepository {
|
|||
private var touchGesturesEnabled = true
|
||||
private var externalPlayerEnabled = false
|
||||
private var externalPlayerForwardSubtitles = false
|
||||
private var externalPlayerSendSkipSegments = false
|
||||
private var externalPlayerId: String? = ExternalPlayerPlatform.defaultPlayerId()
|
||||
private var preferredAudioLanguage = AudioLanguageOption.DEVICE
|
||||
private var secondaryPreferredAudioLanguage: String? = null
|
||||
|
|
@ -172,6 +174,7 @@ object PlayerSettingsRepository {
|
|||
touchGesturesEnabled = true
|
||||
externalPlayerEnabled = false
|
||||
externalPlayerForwardSubtitles = false
|
||||
externalPlayerSendSkipSegments = false
|
||||
externalPlayerId = ExternalPlayerPlatform.defaultPlayerId()
|
||||
preferredAudioLanguage = AudioLanguageOption.DEVICE
|
||||
secondaryPreferredAudioLanguage = null
|
||||
|
|
@ -236,6 +239,7 @@ object PlayerSettingsRepository {
|
|||
touchGesturesEnabled = PlayerSettingsStorage.loadTouchGesturesEnabled() ?: true
|
||||
externalPlayerEnabled = PlayerSettingsStorage.loadExternalPlayerEnabled() ?: false
|
||||
externalPlayerForwardSubtitles = PlayerSettingsStorage.loadExternalPlayerForwardSubtitles() ?: false
|
||||
externalPlayerSendSkipSegments = PlayerSettingsStorage.loadExternalPlayerSendSkipSegments() ?: false
|
||||
externalPlayerId = PlayerSettingsStorage.loadExternalPlayerId()
|
||||
?: ExternalPlayerPlatform.defaultPlayerId()
|
||||
preferredAudioLanguage =
|
||||
|
|
@ -432,6 +436,14 @@ object PlayerSettingsRepository {
|
|||
PlayerSettingsStorage.saveExternalPlayerForwardSubtitles(enabled)
|
||||
}
|
||||
|
||||
fun setExternalPlayerSendSkipSegments(enabled: Boolean) {
|
||||
ensureLoaded()
|
||||
if (externalPlayerSendSkipSegments == enabled) return
|
||||
externalPlayerSendSkipSegments = enabled
|
||||
publish()
|
||||
PlayerSettingsStorage.saveExternalPlayerSendSkipSegments(enabled)
|
||||
}
|
||||
|
||||
fun setPreferredAudioLanguage(language: String) {
|
||||
ensureLoaded()
|
||||
val normalized = normalizeLanguageCode(language) ?: AudioLanguageOption.DEVICE
|
||||
|
|
@ -893,6 +905,7 @@ object PlayerSettingsRepository {
|
|||
touchGesturesEnabled = touchGesturesEnabled,
|
||||
externalPlayerEnabled = externalPlayerEnabled,
|
||||
externalPlayerForwardSubtitles = externalPlayerForwardSubtitles,
|
||||
externalPlayerSendSkipSegments = externalPlayerSendSkipSegments,
|
||||
externalPlayerId = externalPlayerId,
|
||||
preferredAudioLanguage = preferredAudioLanguage,
|
||||
secondaryPreferredAudioLanguage = secondaryPreferredAudioLanguage,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ internal expect object PlayerSettingsStorage {
|
|||
fun saveExternalPlayerEnabled(enabled: Boolean)
|
||||
fun loadExternalPlayerForwardSubtitles(): Boolean?
|
||||
fun saveExternalPlayerForwardSubtitles(enabled: Boolean)
|
||||
fun loadExternalPlayerSendSkipSegments(): Boolean?
|
||||
fun saveExternalPlayerSendSkipSegments(enabled: Boolean)
|
||||
fun loadExternalPlayerId(): String?
|
||||
fun saveExternalPlayerId(playerId: String?)
|
||||
fun loadPreferredAudioLanguage(): String?
|
||||
|
|
|
|||
|
|
@ -12,10 +12,15 @@ object SkipIntroRepository {
|
|||
private val introDbConfigured: Boolean
|
||||
get() = IntroDbConfig.URL.isNotBlank()
|
||||
|
||||
suspend fun getSkipIntervals(imdbId: String?, season: Int, episode: Int): List<SkipInterval> {
|
||||
suspend fun getSkipIntervals(
|
||||
imdbId: String?,
|
||||
season: Int,
|
||||
episode: Int,
|
||||
requireSkipIntroEnabled: Boolean = true,
|
||||
): List<SkipInterval> {
|
||||
if (imdbId == null) return emptyList()
|
||||
val settings = PlayerSettingsRepository.uiState.value
|
||||
if (!settings.skipIntroEnabled) return emptyList()
|
||||
if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList()
|
||||
|
||||
val cacheKey = "$imdbId:$season:$episode"
|
||||
cache[cacheKey]?.let { return it }
|
||||
|
|
@ -46,9 +51,13 @@ object SkipIntroRepository {
|
|||
return emptyList<SkipInterval>().also { cache[cacheKey] = it }
|
||||
}
|
||||
|
||||
suspend fun getSkipIntervalsForMal(malId: String, episode: Int): List<SkipInterval> {
|
||||
suspend fun getSkipIntervalsForMal(
|
||||
malId: String,
|
||||
episode: Int,
|
||||
requireSkipIntroEnabled: Boolean = true,
|
||||
): List<SkipInterval> {
|
||||
val settings = PlayerSettingsRepository.uiState.value
|
||||
if (!settings.skipIntroEnabled) return emptyList()
|
||||
if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList()
|
||||
|
||||
val cacheKey = "mal:$malId:$episode"
|
||||
cache[cacheKey]?.let { return it }
|
||||
|
|
@ -90,9 +99,13 @@ object SkipIntroRepository {
|
|||
return emptyList<SkipInterval>().also { cache[cacheKey] = it }
|
||||
}
|
||||
|
||||
suspend fun getSkipIntervalsForKitsu(kitsuId: String, episode: Int): List<SkipInterval> {
|
||||
suspend fun getSkipIntervalsForKitsu(
|
||||
kitsuId: String,
|
||||
episode: Int,
|
||||
requireSkipIntroEnabled: Boolean = true,
|
||||
): List<SkipInterval> {
|
||||
val settings = PlayerSettingsRepository.uiState.value
|
||||
if (!settings.skipIntroEnabled) return emptyList()
|
||||
if (requireSkipIntroEnabled && !settings.skipIntroEnabled) return emptyList()
|
||||
|
||||
val cacheKey = "kitsu:$kitsuId:$episode"
|
||||
cache[cacheKey]?.let { return it }
|
||||
|
|
|
|||
|
|
@ -368,6 +368,14 @@ private fun PlaybackSettingsSection(
|
|||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setExternalPlayerForwardSubtitles,
|
||||
)
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsSwitchRow(
|
||||
title = stringResource(Res.string.settings_playback_external_player_send_skip_segments),
|
||||
description = stringResource(Res.string.settings_playback_external_player_send_skip_segments_description),
|
||||
checked = autoPlayPlayerSettings.externalPlayerSendSkipSegments,
|
||||
isTablet = isTablet,
|
||||
onCheckedChange = PlayerSettingsRepository::setExternalPlayerSendSkipSegments,
|
||||
)
|
||||
}
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsSwitchRow(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ actual object PlayerSettingsStorage {
|
|||
private const val touchGesturesEnabledKey = "touch_gestures_enabled"
|
||||
private const val externalPlayerEnabledKey = "external_player_enabled"
|
||||
private const val externalPlayerForwardSubtitlesKey = "external_player_forward_subtitles"
|
||||
private const val externalPlayerSendSkipSegmentsKey = "external_player_send_skip_segments"
|
||||
private const val externalPlayerIdKey = "external_player_id"
|
||||
private const val preferredAudioLanguageKey = "preferred_audio_language"
|
||||
private const val secondaryPreferredAudioLanguageKey = "secondary_preferred_audio_language"
|
||||
|
|
@ -91,6 +92,7 @@ actual object PlayerSettingsStorage {
|
|||
touchGesturesEnabledKey,
|
||||
externalPlayerEnabledKey,
|
||||
externalPlayerForwardSubtitlesKey,
|
||||
externalPlayerSendSkipSegmentsKey,
|
||||
externalPlayerIdKey,
|
||||
preferredAudioLanguageKey,
|
||||
secondaryPreferredAudioLanguageKey,
|
||||
|
|
@ -256,6 +258,20 @@ actual object PlayerSettingsStorage {
|
|||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(externalPlayerForwardSubtitlesKey))
|
||||
}
|
||||
|
||||
actual fun loadExternalPlayerSendSkipSegments(): Boolean? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(externalPlayerSendSkipSegmentsKey)
|
||||
return if (defaults.objectForKey(key) != null) {
|
||||
defaults.boolForKey(key)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
actual fun saveExternalPlayerSendSkipSegments(enabled: Boolean) {
|
||||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(externalPlayerSendSkipSegmentsKey))
|
||||
}
|
||||
|
||||
actual fun loadExternalPlayerId(): String? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val key = ProfileScopedKey.of(externalPlayerIdKey)
|
||||
|
|
|
|||
Loading…
Reference in a new issue