Per-stream visibility: isPublic field, Room migration 6→7, publish toggle in CreatePlan

This commit is contained in:
2026-03-03 21:37:20 +01:00
parent ec1b84994b
commit b235eabd40
9 changed files with 57 additions and 6 deletions

View File

@@ -16,7 +16,7 @@ import com.omixlab.lckcontrol.data.local.entity.StreamPlanEntity
StreamPlanEntity::class, StreamPlanEntity::class,
StreamDestinationEntity::class, StreamDestinationEntity::class,
], ],
version = 6, version = 7,
exportSchema = false, exportSchema = false,
) )
abstract class LckDatabase : RoomDatabase() { abstract class LckDatabase : RoomDatabase() {
@@ -116,5 +116,11 @@ abstract class LckDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE linked_accounts ADD COLUMN streamKey TEXT") db.execSQL("ALTER TABLE linked_accounts ADD COLUMN streamKey TEXT")
} }
} }
val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE stream_plans ADD COLUMN isPublic INTEGER NOT NULL DEFAULT 1")
}
}
} }
} }

View File

@@ -10,5 +10,6 @@ data class StreamPlanEntity(
val status: String = "DRAFT", val status: String = "DRAFT",
val executionMode: String = "IN_GAME", val executionMode: String = "IN_GAME",
val gameId: String = "", val gameId: String = "",
val isPublic: Boolean = true,
val createdAt: Long = System.currentTimeMillis(), val createdAt: Long = System.currentTimeMillis(),
) )

View File

@@ -101,6 +101,7 @@ data class CreateStreamPlanRequest(
val name: String, val name: String,
val executionMode: String? = null, val executionMode: String? = null,
val gameId: String? = null, val gameId: String? = null,
val isPublic: Boolean? = null,
val destinations: List<CreateDestinationRequest>, val destinations: List<CreateDestinationRequest>,
) )
@@ -109,6 +110,7 @@ data class UpdateStreamPlanRequest(
val name: String? = null, val name: String? = null,
val executionMode: String? = null, val executionMode: String? = null,
val gameId: String? = null, val gameId: String? = null,
val isPublic: Boolean? = null,
val destinations: List<CreateDestinationRequest>? = null, val destinations: List<CreateDestinationRequest>? = null,
) )
@@ -131,6 +133,7 @@ data class StreamPlanResponse(
val status: String, val status: String,
val executionMode: String? = null, val executionMode: String? = null,
val gameId: String? = null, val gameId: String? = null,
val isPublic: Boolean = true,
val createdAt: String, val createdAt: String,
val updatedAt: String, val updatedAt: String,
val destinations: List<StreamDestinationResponse>, val destinations: List<StreamDestinationResponse>,

View File

@@ -48,11 +48,13 @@ class StreamPlanRepository @Inject constructor(
destinations: List<StreamDestination>, destinations: List<StreamDestination>,
executionMode: String = "IN_GAME", executionMode: String = "IN_GAME",
gameId: String = "", gameId: String = "",
isPublic: Boolean = true,
): StreamPlan { ): StreamPlan {
val request = CreateStreamPlanRequest( val request = CreateStreamPlanRequest(
name = name, name = name,
executionMode = executionMode, executionMode = executionMode,
gameId = gameId.ifBlank { null }, gameId = gameId.ifBlank { null },
isPublic = isPublic,
destinations = destinations.map { dest -> destinations = destinations.map { dest ->
CreateDestinationRequest( CreateDestinationRequest(
linkedAccountId = dest.linkedAccountId.ifBlank { null }, linkedAccountId = dest.linkedAccountId.ifBlank { null },
@@ -78,11 +80,13 @@ class StreamPlanRepository @Inject constructor(
destinations: List<StreamDestination>, destinations: List<StreamDestination>,
executionMode: String, executionMode: String,
gameId: String, gameId: String,
isPublic: Boolean = true,
): StreamPlan { ): StreamPlan {
val request = UpdateStreamPlanRequest( val request = UpdateStreamPlanRequest(
name = name, name = name,
executionMode = executionMode, executionMode = executionMode,
gameId = gameId.ifBlank { null }, gameId = gameId.ifBlank { null },
isPublic = isPublic,
destinations = destinations.map { dest -> destinations = destinations.map { dest ->
CreateDestinationRequest( CreateDestinationRequest(
linkedAccountId = dest.linkedAccountId.ifBlank { null }, linkedAccountId = dest.linkedAccountId.ifBlank { null },
@@ -146,6 +150,7 @@ class StreamPlanRepository @Inject constructor(
status = remote.status, status = remote.status,
executionMode = remote.executionMode ?: "IN_GAME", executionMode = remote.executionMode ?: "IN_GAME",
gameId = remote.gameId ?: "", gameId = remote.gameId ?: "",
isPublic = remote.isPublic,
) )
val destEntities = remote.destinations.map { d -> val destEntities = remote.destinations.map { d ->
StreamDestinationEntity( StreamDestinationEntity(
@@ -173,6 +178,7 @@ class StreamPlanRepository @Inject constructor(
status = plan.status, status = plan.status,
executionMode = plan.executionMode, executionMode = plan.executionMode,
gameId = plan.gameId, gameId = plan.gameId,
isPublic = plan.isPublic,
destinations = destinations.map { it.toStreamDestination() }, destinations = destinations.map { it.toStreamDestination() },
) )

View File

@@ -20,7 +20,7 @@ object DatabaseModule {
@Singleton @Singleton
fun provideDatabase(@ApplicationContext context: Context): LckDatabase = fun provideDatabase(@ApplicationContext context: Context): LckDatabase =
Room.databaseBuilder(context, LckDatabase::class.java, "lck_control.db") Room.databaseBuilder(context, LckDatabase::class.java, "lck_control.db")
.addMigrations(LckDatabase.MIGRATION_1_2, LckDatabase.MIGRATION_2_3, LckDatabase.MIGRATION_3_4, LckDatabase.MIGRATION_4_5, LckDatabase.MIGRATION_5_6) .addMigrations(LckDatabase.MIGRATION_1_2, LckDatabase.MIGRATION_2_3, LckDatabase.MIGRATION_3_4, LckDatabase.MIGRATION_4_5, LckDatabase.MIGRATION_5_6, LckDatabase.MIGRATION_6_7)
.build() .build()
@Provides @Provides

View File

@@ -206,9 +206,9 @@ fun DashboardScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text("Show on Portal", style = MaterialTheme.typography.titleSmall) Text("Public Profile", style = MaterialTheme.typography.titleSmall)
Text( Text(
"Allow others to discover your live streams", "Allow others to find your profile",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )

View File

@@ -30,6 +30,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
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
@@ -38,6 +39,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -56,6 +58,7 @@ fun CreatePlanScreen(
val planName by viewModel.planName.collectAsStateWithLifecycle() val planName by viewModel.planName.collectAsStateWithLifecycle()
val executionMode by viewModel.executionMode.collectAsStateWithLifecycle() val executionMode by viewModel.executionMode.collectAsStateWithLifecycle()
val gameId by viewModel.gameId.collectAsStateWithLifecycle() val gameId by viewModel.gameId.collectAsStateWithLifecycle()
val isPublic by viewModel.isPublic.collectAsStateWithLifecycle()
val connectedClients by viewModel.connectedClients.collectAsStateWithLifecycle() val connectedClients by viewModel.connectedClients.collectAsStateWithLifecycle()
val destinations by viewModel.destinations.collectAsStateWithLifecycle() val destinations by viewModel.destinations.collectAsStateWithLifecycle()
val linkedAccounts by viewModel.linkedAccounts.collectAsStateWithLifecycle() val linkedAccounts by viewModel.linkedAccounts.collectAsStateWithLifecycle()
@@ -101,6 +104,27 @@ fun CreatePlanScreen(
) )
} }
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("Publish to Portal", style = MaterialTheme.typography.titleSmall)
Text(
"Show this stream in the public feed",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(
checked = isPublic,
onCheckedChange = viewModel::setIsPublic,
)
}
}
item { item {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text("Execution Mode", style = MaterialTheme.typography.titleMedium) Text("Execution Mode", style = MaterialTheme.typography.titleMedium)

View File

@@ -66,6 +66,9 @@ class CreatePlanViewModel @Inject constructor(
private val _gameId = MutableStateFlow("") private val _gameId = MutableStateFlow("")
val gameId: StateFlow<String> = _gameId.asStateFlow() val gameId: StateFlow<String> = _gameId.asStateFlow()
private val _isPublic = MutableStateFlow(true)
val isPublic: StateFlow<Boolean> = _isPublic.asStateFlow()
private val _connectedClients = MutableStateFlow<List<ConnectedClientInfo>>(emptyList()) private val _connectedClients = MutableStateFlow<List<ConnectedClientInfo>>(emptyList())
val connectedClients: StateFlow<List<ConnectedClientInfo>> = _connectedClients.asStateFlow() val connectedClients: StateFlow<List<ConnectedClientInfo>> = _connectedClients.asStateFlow()
@@ -134,6 +137,7 @@ class CreatePlanViewModel @Inject constructor(
_planName.value = plan.name _planName.value = plan.name
_executionMode.value = plan.executionMode _executionMode.value = plan.executionMode
_gameId.value = plan.gameId _gameId.value = plan.gameId
_isPublic.value = plan.isPublic
// Wait for linked accounts to load for label resolution // Wait for linked accounts to load for label resolution
val accounts = accountRepository.getAccounts() val accounts = accountRepository.getAccounts()
@@ -172,6 +176,10 @@ class CreatePlanViewModel @Inject constructor(
_gameId.value = gameId _gameId.value = gameId
} }
fun setIsPublic(isPublic: Boolean) {
_isPublic.value = isPublic
}
fun addDestination() { fun addDestination() {
_destinations.value = _destinations.value + DestinationInput() _destinations.value = _destinations.value + DestinationInput()
} }
@@ -243,9 +251,9 @@ class CreatePlanViewModel @Inject constructor(
} }
} }
val plan = if (isEditMode) { val plan = if (isEditMode) {
streamPlanRepository.updatePlan(editingPlanId!!, name, streamDests, _executionMode.value, _gameId.value) streamPlanRepository.updatePlan(editingPlanId!!, name, streamDests, _executionMode.value, _gameId.value, _isPublic.value)
} else { } else {
streamPlanRepository.createPlan(name, streamDests, _executionMode.value, _gameId.value) streamPlanRepository.createPlan(name, streamDests, _executionMode.value, _gameId.value, _isPublic.value)
} }
onSaved(plan.planId) onSaved(plan.planId)
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -10,6 +10,7 @@ data class StreamPlan(
val destinations: List<StreamDestination> = emptyList(), val destinations: List<StreamDestination> = emptyList(),
val executionMode: String = "IN_GAME", val executionMode: String = "IN_GAME",
val gameId: String = "", val gameId: String = "",
val isPublic: Boolean = true,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
@@ -19,6 +20,7 @@ data class StreamPlan(
destinations = parcel.createTypedArrayList(StreamDestination.CREATOR) ?: emptyList(), destinations = parcel.createTypedArrayList(StreamDestination.CREATOR) ?: emptyList(),
executionMode = if (parcel.dataAvail() > 0) parcel.readString() ?: "IN_GAME" else "IN_GAME", executionMode = if (parcel.dataAvail() > 0) parcel.readString() ?: "IN_GAME" else "IN_GAME",
gameId = if (parcel.dataAvail() > 0) parcel.readString() ?: "" else "", gameId = if (parcel.dataAvail() > 0) parcel.readString() ?: "" else "",
isPublic = if (parcel.dataAvail() > 0) parcel.readInt() == 1 else true,
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -28,6 +30,7 @@ data class StreamPlan(
parcel.writeTypedList(destinations) parcel.writeTypedList(destinations)
parcel.writeString(executionMode) parcel.writeString(executionMode)
parcel.writeString(gameId) parcel.writeString(gameId)
parcel.writeInt(if (isPublic) 1 else 0)
} }
override fun describeContents(): Int = 0 override fun describeContents(): Int = 0