Pairing code generation in Accounts tab, portal chat support
- Add PairingCodeResponse, PairingStatusResponse models + API endpoints - Add pairing code card to Accounts screen with countdown + auto-dismiss on redeem - Add Portal service color/label to chat and dashboard live section - Move portal visibility toggle to dashboard, add UpdateProfileRequest
This commit is contained in:
@@ -38,6 +38,28 @@ data class UserProfileResponse(
|
|||||||
val displayName: String,
|
val displayName: String,
|
||||||
val email: String?,
|
val email: String?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
|
val bio: String = "",
|
||||||
|
val isPublic: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PairingCodeResponse(
|
||||||
|
val code: String,
|
||||||
|
val expiresAt: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class PairingStatusResponse(
|
||||||
|
val active: Boolean,
|
||||||
|
val code: String? = null,
|
||||||
|
val expiresAt: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class UpdateProfileRequest(
|
||||||
|
val displayName: String? = null,
|
||||||
|
val bio: String? = null,
|
||||||
|
val isPublic: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Providers ────────────────────────────────────────────
|
// ── Providers ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ interface LckApiService {
|
|||||||
@POST("auth/logout")
|
@POST("auth/logout")
|
||||||
suspend fun logout(): SuccessResponse
|
suspend fun logout(): SuccessResponse
|
||||||
|
|
||||||
|
@PATCH("auth/me")
|
||||||
|
suspend fun updateProfile(@Body body: UpdateProfileRequest): UserProfileResponse
|
||||||
|
|
||||||
|
@POST("auth/pairing/generate")
|
||||||
|
suspend fun generatePairingCode(): PairingCodeResponse
|
||||||
|
|
||||||
|
@GET("auth/pairing/status")
|
||||||
|
suspend fun getPairingStatus(): PairingStatusResponse
|
||||||
|
|
||||||
// ── Providers ────────────────────────────────────────
|
// ── Providers ────────────────────────────────────────
|
||||||
|
|
||||||
@GET("providers/accounts")
|
@GET("providers/accounts")
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.LinkOff
|
import androidx.compose.material.icons.filled.LinkOff
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -36,7 +40,13 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
@@ -52,6 +62,9 @@ fun AccountsScreen(
|
|||||||
val customUrl by viewModel.customRtmpUrl.collectAsStateWithLifecycle()
|
val customUrl by viewModel.customRtmpUrl.collectAsStateWithLifecycle()
|
||||||
val customKey by viewModel.customRtmpKey.collectAsStateWithLifecycle()
|
val customKey by viewModel.customRtmpKey.collectAsStateWithLifecycle()
|
||||||
val isCreating by viewModel.isCreatingCustomRtmp.collectAsStateWithLifecycle()
|
val isCreating by viewModel.isCreatingCustomRtmp.collectAsStateWithLifecycle()
|
||||||
|
val pairingCode by viewModel.pairingCode.collectAsStateWithLifecycle()
|
||||||
|
val pairingExpiresAt by viewModel.pairingExpiresAt.collectAsStateWithLifecycle()
|
||||||
|
val pairingLoading by viewModel.pairingLoading.collectAsStateWithLifecycle()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -123,6 +136,18 @@ fun AccountsScreen(
|
|||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text("Portal Pairing", style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
PairingCodeCard(
|
||||||
|
code = pairingCode,
|
||||||
|
expiresAt = pairingExpiresAt,
|
||||||
|
loading = pairingLoading,
|
||||||
|
onGenerate = viewModel::generatePairingCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(8.dp)) }
|
item { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
items(accounts, key = { it.id }) { account ->
|
items(accounts, key = { it.id }) { account ->
|
||||||
@@ -188,3 +213,93 @@ fun AccountsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PairingCodeCard(
|
||||||
|
code: String?,
|
||||||
|
expiresAt: Long?,
|
||||||
|
loading: Boolean,
|
||||||
|
onGenerate: () -> Unit,
|
||||||
|
) {
|
||||||
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
if (code != null && expiresAt != null) {
|
||||||
|
val remainingSeconds = ((expiresAt - System.currentTimeMillis()) / 1000).coerceAtLeast(0)
|
||||||
|
|
||||||
|
// Countdown ticker
|
||||||
|
val tickState = remember { MutableStateFlow(remainingSeconds) }
|
||||||
|
val currentRemaining by tickState.collectAsStateWithLifecycle()
|
||||||
|
LaunchedEffect(expiresAt) {
|
||||||
|
while (true) {
|
||||||
|
val r = ((expiresAt - System.currentTimeMillis()) / 1000).coerceAtLeast(0)
|
||||||
|
tickState.value = r
|
||||||
|
if (r <= 0) break
|
||||||
|
delay(1_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRemaining > 0) {
|
||||||
|
Text(
|
||||||
|
"Enter this code on the portal:",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = code.chunked(3).joinToString(" "),
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
letterSpacing = androidx.compose.ui.unit.TextUnit(4f, androidx.compose.ui.unit.TextUnitType.Sp),
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
val minutes = currentRemaining / 60
|
||||||
|
val seconds = currentRemaining % 60
|
||||||
|
Text(
|
||||||
|
text = "Expires in %d:%02d".format(minutes, seconds),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (currentRemaining < 60) MaterialTheme.colorScheme.error
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
OutlinedButton(onClick = onGenerate, enabled = !loading) {
|
||||||
|
Text("Generate New Code")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"Code expired",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Button(onClick = onGenerate, enabled = !loading) {
|
||||||
|
Text("Generate New Code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
"Link your portal account by generating a pairing code",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Button(onClick = onGenerate, enabled = !loading) {
|
||||||
|
if (loading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Text("Link Portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import android.net.Uri
|
|||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.omixlab.lckcontrol.data.remote.LckApiService
|
||||||
import com.omixlab.lckcontrol.data.repository.AccountRepository
|
import com.omixlab.lckcontrol.data.repository.AccountRepository
|
||||||
import com.omixlab.lckcontrol.shared.LinkedAccount
|
import com.omixlab.lckcontrol.shared.LinkedAccount
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -29,6 +32,7 @@ val ALL_PROVIDERS = listOf(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AccountsViewModel @Inject constructor(
|
class AccountsViewModel @Inject constructor(
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
|
private val apiService: LckApiService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val accounts: StateFlow<List<LinkedAccount>> = accountRepository.observeAccounts()
|
val accounts: StateFlow<List<LinkedAccount>> = accountRepository.observeAccounts()
|
||||||
@@ -53,6 +57,18 @@ class AccountsViewModel @Inject constructor(
|
|||||||
private val _isCreatingCustomRtmp = MutableStateFlow(false)
|
private val _isCreatingCustomRtmp = MutableStateFlow(false)
|
||||||
val isCreatingCustomRtmp: StateFlow<Boolean> = _isCreatingCustomRtmp.asStateFlow()
|
val isCreatingCustomRtmp: StateFlow<Boolean> = _isCreatingCustomRtmp.asStateFlow()
|
||||||
|
|
||||||
|
// Pairing code state
|
||||||
|
private val _pairingCode = MutableStateFlow<String?>(null)
|
||||||
|
val pairingCode: StateFlow<String?> = _pairingCode.asStateFlow()
|
||||||
|
|
||||||
|
private val _pairingExpiresAt = MutableStateFlow<Long?>(null)
|
||||||
|
val pairingExpiresAt: StateFlow<Long?> = _pairingExpiresAt.asStateFlow()
|
||||||
|
|
||||||
|
private val _pairingLoading = MutableStateFlow(false)
|
||||||
|
val pairingLoading: StateFlow<Boolean> = _pairingLoading.asStateFlow()
|
||||||
|
|
||||||
|
private var pairingPollJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Sync accounts from backend on load
|
// Sync accounts from backend on load
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -140,4 +156,40 @@ class AccountsViewModel @Inject constructor(
|
|||||||
fun clearError() {
|
fun clearError() {
|
||||||
_linkError.value = null
|
_linkError.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun generatePairingCode() {
|
||||||
|
_pairingLoading.value = true
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val response = apiService.generatePairingCode()
|
||||||
|
_pairingCode.value = response.code
|
||||||
|
_pairingExpiresAt.value = java.time.Instant.parse(response.expiresAt).toEpochMilli()
|
||||||
|
startPairingPoll()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_pairingCode.value = null
|
||||||
|
_pairingExpiresAt.value = null
|
||||||
|
} finally {
|
||||||
|
_pairingLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPairingPoll() {
|
||||||
|
pairingPollJob?.cancel()
|
||||||
|
pairingPollJob = viewModelScope.launch {
|
||||||
|
while (true) {
|
||||||
|
delay(3_000)
|
||||||
|
try {
|
||||||
|
val status = apiService.getPairingStatus()
|
||||||
|
if (!status.active) {
|
||||||
|
_pairingCode.value = null
|
||||||
|
_pairingExpiresAt.value = null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
val expiresAt = _pairingExpiresAt.value ?: break
|
||||||
|
if (System.currentTimeMillis() >= expiresAt) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import java.util.Locale
|
|||||||
|
|
||||||
private val YouTubeRed = Color(0xFFFF0000)
|
private val YouTubeRed = Color(0xFFFF0000)
|
||||||
private val TwitchPurple = Color(0xFF9146FF)
|
private val TwitchPurple = Color(0xFF9146FF)
|
||||||
|
private val PortalTeal = Color(0xFF00BCD4)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -67,8 +68,18 @@ fun ChatScreen(
|
|||||||
val status = connectionMap[chatKey]
|
val status = connectionMap[chatKey]
|
||||||
val isConnected = status?.connected ?: false
|
val isConnected = status?.connected ?: false
|
||||||
|
|
||||||
val serviceColor = if (viewModel.service == "YOUTUBE") YouTubeRed else TwitchPurple
|
val serviceColor = when (viewModel.service) {
|
||||||
val serviceLabel = if (viewModel.service == "YOUTUBE") "YouTube Chat" else "Twitch Chat"
|
"YOUTUBE" -> YouTubeRed
|
||||||
|
"TWITCH" -> TwitchPurple
|
||||||
|
"PORTAL" -> PortalTeal
|
||||||
|
else -> TwitchPurple
|
||||||
|
}
|
||||||
|
val serviceLabel = when (viewModel.service) {
|
||||||
|
"YOUTUBE" -> "YouTube Chat"
|
||||||
|
"TWITCH" -> "Twitch Chat"
|
||||||
|
"PORTAL" -> "Portal Comments"
|
||||||
|
else -> "${viewModel.service} Chat"
|
||||||
|
}
|
||||||
|
|
||||||
var inputText by remember { mutableStateOf("") }
|
var inputText by remember { mutableStateOf("") }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -71,6 +72,7 @@ fun DashboardScreen(
|
|||||||
val unreadCounts by viewModel.unreadCounts.collectAsStateWithLifecycle()
|
val unreadCounts by viewModel.unreadCounts.collectAsStateWithLifecycle()
|
||||||
val chatStatus by viewModel.chatConnectionStatus.collectAsStateWithLifecycle()
|
val chatStatus by viewModel.chatConnectionStatus.collectAsStateWithLifecycle()
|
||||||
val accountNames by viewModel.accountDisplayNames.collectAsStateWithLifecycle()
|
val accountNames by viewModel.accountDisplayNames.collectAsStateWithLifecycle()
|
||||||
|
val isPublic by viewModel.isPublic.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -151,9 +153,17 @@ fun DashboardScreen(
|
|||||||
// Live destinations section — shows each service with chat + browser buttons
|
// Live destinations section — shows each service with chat + browser buttons
|
||||||
val liveDestinations = plans.flatMap { plan ->
|
val liveDestinations = plans.flatMap { plan ->
|
||||||
if (plan.status == "LIVE") {
|
if (plan.status == "LIVE") {
|
||||||
plan.destinations.filter {
|
val dests = plan.destinations.filter {
|
||||||
it.service == "YOUTUBE" || it.service == "TWITCH"
|
it.service == "YOUTUBE" || it.service == "TWITCH"
|
||||||
}.map { dest -> Triple(plan.planId, dest.service, dest) }
|
}.map { dest -> Triple(plan.planId, dest.service, dest) }
|
||||||
|
// Add synthetic PORTAL destination for live plans
|
||||||
|
val portalDest = com.omixlab.lckcontrol.shared.StreamDestination(
|
||||||
|
service = "PORTAL",
|
||||||
|
linkedAccountId = "portal",
|
||||||
|
title = "Portal Comments",
|
||||||
|
status = "LIVE",
|
||||||
|
)
|
||||||
|
dests + Triple(plan.planId, "PORTAL", portalDest)
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
}
|
}
|
||||||
if (liveDestinations.isNotEmpty()) {
|
if (liveDestinations.isNotEmpty()) {
|
||||||
@@ -189,6 +199,28 @@ fun DashboardScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text("Show on Portal", style = MaterialTheme.typography.titleSmall)
|
||||||
|
Text(
|
||||||
|
"Allow others to discover your live streams",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = isPublic,
|
||||||
|
onCheckedChange = viewModel::setPublicVisibility,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("Default Streaming Mode", style = MaterialTheme.typography.titleMedium)
|
Text("Default Streaming Mode", style = MaterialTheme.typography.titleMedium)
|
||||||
@@ -300,6 +332,7 @@ private fun PlanCard(
|
|||||||
|
|
||||||
private val YouTubeRed = Color(0xFFFF0000)
|
private val YouTubeRed = Color(0xFFFF0000)
|
||||||
private val TwitchPurple = Color(0xFF9146FF)
|
private val TwitchPurple = Color(0xFF9146FF)
|
||||||
|
private val PortalTeal = Color(0xFF00BCD4)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LiveDestinationCard(
|
private fun LiveDestinationCard(
|
||||||
@@ -310,8 +343,18 @@ private fun LiveDestinationCard(
|
|||||||
onChatClick: () -> Unit,
|
onChatClick: () -> Unit,
|
||||||
onBrowserClick: () -> Unit,
|
onBrowserClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val serviceColor = if (service == "YOUTUBE") YouTubeRed else TwitchPurple
|
val serviceColor = when (service) {
|
||||||
val serviceLabel = if (service == "YOUTUBE") "YouTube" else "Twitch"
|
"YOUTUBE" -> YouTubeRed
|
||||||
|
"TWITCH" -> TwitchPurple
|
||||||
|
"PORTAL" -> PortalTeal
|
||||||
|
else -> TwitchPurple
|
||||||
|
}
|
||||||
|
val serviceLabel = when (service) {
|
||||||
|
"YOUTUBE" -> "YouTube"
|
||||||
|
"TWITCH" -> "Twitch"
|
||||||
|
"PORTAL" -> "Portal"
|
||||||
|
else -> service
|
||||||
|
}
|
||||||
val hasFailed = connectionStatus != null && !connectionStatus.connected && connectionStatus.error != null
|
val hasFailed = connectionStatus != null && !connectionStatus.connected && connectionStatus.error != null
|
||||||
|
|
||||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
@@ -366,12 +409,14 @@ private fun LiveDestinationCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = onBrowserClick) {
|
if (service != "PORTAL") {
|
||||||
Icon(
|
IconButton(onClick = onBrowserClick) {
|
||||||
Icons.Default.OpenInBrowser,
|
Icon(
|
||||||
contentDescription = "Open in browser",
|
Icons.Default.OpenInBrowser,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
contentDescription = "Open in browser",
|
||||||
)
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.omixlab.lckcontrol.data.remote.LckApiService
|
|||||||
import com.omixlab.lckcontrol.data.repository.AccountRepository
|
import com.omixlab.lckcontrol.data.repository.AccountRepository
|
||||||
import com.omixlab.lckcontrol.data.repository.ChatRepository
|
import com.omixlab.lckcontrol.data.repository.ChatRepository
|
||||||
import com.omixlab.lckcontrol.data.repository.StreamPlanRepository
|
import com.omixlab.lckcontrol.data.repository.StreamPlanRepository
|
||||||
|
import com.omixlab.lckcontrol.data.remote.UpdateProfileRequest
|
||||||
import com.omixlab.lckcontrol.shared.StreamPlan
|
import com.omixlab.lckcontrol.shared.StreamPlan
|
||||||
import com.omixlab.lckcontrol.streaming.StreamingManager
|
import com.omixlab.lckcontrol.streaming.StreamingManager
|
||||||
import com.omixlab.lckcontrol.streaming.StreamingState
|
import com.omixlab.lckcontrol.streaming.StreamingState
|
||||||
@@ -57,10 +58,20 @@ class DashboardViewModel @Inject constructor(
|
|||||||
private val _defaultExecutionMode = MutableStateFlow(appPreferences.getDefaultExecutionMode())
|
private val _defaultExecutionMode = MutableStateFlow(appPreferences.getDefaultExecutionMode())
|
||||||
val defaultExecutionMode: StateFlow<String> = _defaultExecutionMode.asStateFlow()
|
val defaultExecutionMode: StateFlow<String> = _defaultExecutionMode.asStateFlow()
|
||||||
|
|
||||||
|
private val _isPublic = MutableStateFlow(false)
|
||||||
|
val isPublic: StateFlow<Boolean> = _isPublic.asStateFlow()
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try { streamPlanRepository.syncPlans() } catch (_: Exception) {}
|
try { streamPlanRepository.syncPlans() } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val profile = apiService.getMe()
|
||||||
|
_isPublic.value = profile.isPublic
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val accounts = accountRepository.getAccounts()
|
val accounts = accountRepository.getAccounts()
|
||||||
@@ -86,6 +97,17 @@ class DashboardViewModel @Inject constructor(
|
|||||||
appPreferences.setDefaultExecutionMode(mode)
|
appPreferences.setDefaultExecutionMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPublicVisibility(isPublic: Boolean) {
|
||||||
|
_isPublic.value = isPublic
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
apiService.updateProfile(UpdateProfileRequest(isPublic = isPublic))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
_isPublic.value = !isPublic // revert on error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearStalePlans() {
|
fun clearStalePlans() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val stale = plans.value.filter { it.status == "ENDED" || it.status == "DRAFT" }
|
val stale = plans.value.filter { it.status == "ENDED" || it.status == "DRAFT" }
|
||||||
|
|||||||
Reference in New Issue
Block a user