feat(android): Phase 10 Commit D2 — Admin Quarantine screen
Replaces the AdminQuarantine-route ComingSoon stub with the real
admin-side queue. Mirrors `flutter_client/lib/admin/admin_quarantine_screen.dart`
+ `AdminQuarantineController`.
New:
- models/wire/AdminQuarantineWire.kt — AdminQuarantineReportWire +
AdminQuarantineItemWire (per-user report and the aggregated row
that collapses reports into per-reason counts).
- models/AdminQuarantineItem.kt — domain refs.
`topReasonSummary` returns the most-cited reason with a "(+N more)"
suffix when other distinct reasons exist.
- api/endpoints/AdminQuarantineApi.kt — Retrofit list + the three
resolution endpoints (resolve / delete-file / delete-via-lidarr).
- admin/data/AdminQuarantineRepository.kt — list + three actions
with internal mappers; same point-and-shoot pattern as
AdminRequestsRepository (no offline queue).
- admin/ui/AdminQuarantineViewModel.kt — VM + UiState in sibling
file. `act()` collapses the three resolution paths into one
optimistic-remove-then-refetch-on-failure helper.
- admin/ui/AdminQuarantineScreen.kt — Scaffold + TopAppBar with
back button + MainAppBarActions. Each row shows track title +
"artist · album" subtitle + report-count pill + top-reason pill +
three action buttons (Resolve / Delete file / Delete + Lidarr).
Modified:
- nav/MinstrelNavGraph.kt — AdminQuarantine route now renders
`AdminQuarantineScreen(navController = navController)` inside
ShellScaffold.
AdminUsers + AdminLanding + Invites still ComingSoon — D3 lands them
together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package com.fabledsword.minstrel.admin.data
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.api.endpoints.AdminQuarantineApi
|
||||||
|
import com.fabledsword.minstrel.models.AdminQuarantineItemRef
|
||||||
|
import com.fabledsword.minstrel.models.AdminQuarantineReportRef
|
||||||
|
import com.fabledsword.minstrel.models.wire.AdminQuarantineItemWire
|
||||||
|
import com.fabledsword.minstrel.models.wire.AdminQuarantineReportWire
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.create
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-through accessor for the admin quarantine queue. Mirrors the
|
||||||
|
* Flutter `AdminQuarantineController`. No Room cache — same rationale
|
||||||
|
* as AdminRequests. Three resolution actions all delete the row from
|
||||||
|
* the queue once the server accepts them.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class AdminQuarantineRepository @Inject constructor(
|
||||||
|
retrofit: Retrofit,
|
||||||
|
) {
|
||||||
|
private val api: AdminQuarantineApi = retrofit.create()
|
||||||
|
|
||||||
|
suspend fun list(): List<AdminQuarantineItemRef> = api.list().map { it.toDomain() }
|
||||||
|
|
||||||
|
suspend fun resolve(trackId: String) = api.resolve(trackId)
|
||||||
|
|
||||||
|
suspend fun deleteFile(trackId: String) = api.deleteFile(trackId)
|
||||||
|
|
||||||
|
suspend fun deleteViaLidarr(trackId: String) = api.deleteViaLidarr(trackId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mappers ──
|
||||||
|
|
||||||
|
private fun AdminQuarantineReportWire.toDomain(): AdminQuarantineReportRef =
|
||||||
|
AdminQuarantineReportRef(
|
||||||
|
userId = userId,
|
||||||
|
username = username,
|
||||||
|
reason = reason,
|
||||||
|
notes = notes,
|
||||||
|
createdAt = createdAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun AdminQuarantineItemWire.toDomain(): AdminQuarantineItemRef =
|
||||||
|
AdminQuarantineItemRef(
|
||||||
|
trackId = trackId,
|
||||||
|
trackTitle = trackTitle,
|
||||||
|
artistName = artistName,
|
||||||
|
albumTitle = albumTitle,
|
||||||
|
albumId = albumId,
|
||||||
|
lidarrAlbumMbid = lidarrAlbumMbid,
|
||||||
|
reportCount = reportCount,
|
||||||
|
latestAt = latestAt,
|
||||||
|
reasonCounts = reasonCounts,
|
||||||
|
reports = reports.map { it.toDomain() },
|
||||||
|
)
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.fabledsword.minstrel.admin.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import com.composables.icons.lucide.ArrowLeft
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.fabledsword.minstrel.models.AdminQuarantineItemRef
|
||||||
|
import com.fabledsword.minstrel.nav.AdminQuarantine
|
||||||
|
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||||
|
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AdminQuarantineScreen(
|
||||||
|
navController: NavHostController,
|
||||||
|
viewModel: AdminQuarantineViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Admin · Quarantine") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(Lucide.ArrowLeft, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
MainAppBarActions(
|
||||||
|
navController = navController,
|
||||||
|
currentRouteName = AdminQuarantine::class.qualifiedName,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { inner ->
|
||||||
|
Box(modifier = Modifier.fillMaxSize().padding(inner)) {
|
||||||
|
when (val s = state) {
|
||||||
|
AdminQuarantineUiState.Loading -> LoadingCentered()
|
||||||
|
AdminQuarantineUiState.Empty -> EmptyState(
|
||||||
|
title = "Queue is empty",
|
||||||
|
body = "When users flag tracks as bad rips, wrong tags, or " +
|
||||||
|
"duplicates, their reports get aggregated and surfaced here.",
|
||||||
|
)
|
||||||
|
is AdminQuarantineUiState.Error -> EmptyState(
|
||||||
|
title = "Couldn't load queue",
|
||||||
|
body = s.message,
|
||||||
|
)
|
||||||
|
is AdminQuarantineUiState.Success -> QueueList(
|
||||||
|
rows = s.rows,
|
||||||
|
onResolve = viewModel::resolve,
|
||||||
|
onDeleteFile = viewModel::deleteFile,
|
||||||
|
onDeleteViaLidarr = viewModel::deleteViaLidarr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QueueList(
|
||||||
|
rows: List<AdminQuarantineItemRef>,
|
||||||
|
onResolve: (String) -> Unit,
|
||||||
|
onDeleteFile: (String) -> Unit,
|
||||||
|
onDeleteViaLidarr: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(items = rows, key = { it.trackId }) { row ->
|
||||||
|
QuarantineRow(
|
||||||
|
row = row,
|
||||||
|
onResolve = { onResolve(row.trackId) },
|
||||||
|
onDeleteFile = { onDeleteFile(row.trackId) },
|
||||||
|
onDeleteViaLidarr = { onDeleteViaLidarr(row.trackId) },
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QuarantineRow(
|
||||||
|
row: AdminQuarantineItemRef,
|
||||||
|
onResolve: () -> Unit,
|
||||||
|
onDeleteFile: () -> Unit,
|
||||||
|
onDeleteViaLidarr: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = row.trackTitle,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${row.artistName} · ${row.albumTitle}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = "${row.reportCount} reports",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (row.topReasonSummary.isNotEmpty()) {
|
||||||
|
AssistChip(
|
||||||
|
onClick = {},
|
||||||
|
enabled = false,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = row.topReasonSummary,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionButtons(
|
||||||
|
onResolve = onResolve,
|
||||||
|
onDeleteFile = onDeleteFile,
|
||||||
|
onDeleteViaLidarr = onDeleteViaLidarr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ActionButtons(
|
||||||
|
onResolve: () -> Unit,
|
||||||
|
onDeleteFile: () -> Unit,
|
||||||
|
onDeleteViaLidarr: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onResolve) { Text("Resolve") }
|
||||||
|
OutlinedButton(onClick = onDeleteFile) { Text("Delete file") }
|
||||||
|
Button(onClick = onDeleteViaLidarr) { Text("Delete + Lidarr") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingCentered() {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package com.fabledsword.minstrel.admin.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.fabledsword.minstrel.admin.data.AdminQuarantineRepository
|
||||||
|
import com.fabledsword.minstrel.models.AdminQuarantineItemRef
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
sealed interface AdminQuarantineUiState {
|
||||||
|
data object Loading : AdminQuarantineUiState
|
||||||
|
data object Empty : AdminQuarantineUiState
|
||||||
|
data class Success(val rows: List<AdminQuarantineItemRef>) : AdminQuarantineUiState
|
||||||
|
data class Error(val message: String) : AdminQuarantineUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AdminQuarantineViewModel @Inject constructor(
|
||||||
|
private val repository: AdminQuarantineRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val internal = MutableStateFlow<AdminQuarantineUiState>(AdminQuarantineUiState.Loading)
|
||||||
|
val uiState: StateFlow<AdminQuarantineUiState> = internal.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
internal.value = AdminQuarantineUiState.Loading
|
||||||
|
try {
|
||||||
|
val rows = repository.list()
|
||||||
|
internal.value = if (rows.isEmpty()) {
|
||||||
|
AdminQuarantineUiState.Empty
|
||||||
|
} else {
|
||||||
|
AdminQuarantineUiState.Success(rows)
|
||||||
|
}
|
||||||
|
} catch (
|
||||||
|
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||||
|
) {
|
||||||
|
internal.value = AdminQuarantineUiState.Error(
|
||||||
|
e.message ?: "Admin quarantine load failed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolve(trackId: String) = act(trackId) { repository.resolve(it) }
|
||||||
|
fun deleteFile(trackId: String) = act(trackId) { repository.deleteFile(it) }
|
||||||
|
fun deleteViaLidarr(trackId: String) = act(trackId) { repository.deleteViaLidarr(it) }
|
||||||
|
|
||||||
|
private fun act(trackId: String, action: suspend (String) -> Unit) {
|
||||||
|
val before = internal.value
|
||||||
|
if (before is AdminQuarantineUiState.Success) {
|
||||||
|
val filtered = before.rows.filterNot { it.trackId == trackId }
|
||||||
|
internal.value = if (filtered.isEmpty()) {
|
||||||
|
AdminQuarantineUiState.Empty
|
||||||
|
} else {
|
||||||
|
AdminQuarantineUiState.Success(filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
action(trackId)
|
||||||
|
} catch (
|
||||||
|
@Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
|
||||||
|
) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.fabledsword.minstrel.api.endpoints
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.models.wire.AdminQuarantineItemWire
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrofit interface for `/api/admin/quarantine`. Mirrors
|
||||||
|
* `flutter_client/lib/api/endpoints/admin_quarantine.dart`.
|
||||||
|
*
|
||||||
|
* Three resolution endpoints:
|
||||||
|
* - `resolve` → admin reviewed, no action taken (clears flags).
|
||||||
|
* - `delete-file` → drop the local file; Lidarr keeps the track.
|
||||||
|
* - `delete-via-lidarr` → ask Lidarr to remove the track upstream too.
|
||||||
|
*/
|
||||||
|
interface AdminQuarantineApi {
|
||||||
|
@GET("api/admin/quarantine")
|
||||||
|
suspend fun list(): List<AdminQuarantineItemWire>
|
||||||
|
|
||||||
|
@POST("api/admin/quarantine/{trackId}/resolve")
|
||||||
|
suspend fun resolve(@Path("trackId") trackId: String)
|
||||||
|
|
||||||
|
@POST("api/admin/quarantine/{trackId}/delete-file")
|
||||||
|
suspend fun deleteFile(@Path("trackId") trackId: String)
|
||||||
|
|
||||||
|
@POST("api/admin/quarantine/{trackId}/delete-via-lidarr")
|
||||||
|
suspend fun deleteViaLidarr(@Path("trackId") trackId: String)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.fabledsword.minstrel.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One user's quarantine report under an aggregated row.
|
||||||
|
*/
|
||||||
|
data class AdminQuarantineReportRef(
|
||||||
|
val userId: String,
|
||||||
|
val username: String,
|
||||||
|
val reason: String,
|
||||||
|
val notes: String? = null,
|
||||||
|
val createdAt: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregated quarantine row in the admin queue — all reports against a
|
||||||
|
* single track collapsed into per-reason counts plus the underlying
|
||||||
|
* per-user reports. Mirrors Flutter's `AdminQuarantineItem`.
|
||||||
|
*
|
||||||
|
* `topReasonSummary` returns the most-cited reason, with a "(+N more)"
|
||||||
|
* suffix when other distinct reasons exist.
|
||||||
|
*/
|
||||||
|
data class AdminQuarantineItemRef(
|
||||||
|
val trackId: String,
|
||||||
|
val trackTitle: String,
|
||||||
|
val artistName: String,
|
||||||
|
val albumTitle: String,
|
||||||
|
val albumId: String,
|
||||||
|
val lidarrAlbumMbid: String? = null,
|
||||||
|
val reportCount: Int = 0,
|
||||||
|
val latestAt: String = "",
|
||||||
|
val reasonCounts: Map<String, Int> = emptyMap(),
|
||||||
|
val reports: List<AdminQuarantineReportRef> = emptyList(),
|
||||||
|
) {
|
||||||
|
val topReasonSummary: String
|
||||||
|
get() {
|
||||||
|
if (reasonCounts.isEmpty()) return ""
|
||||||
|
val sorted = reasonCounts.entries.sortedByDescending { it.value }
|
||||||
|
val top = sorted.first().key
|
||||||
|
return if (sorted.size > 1) "$top (+${sorted.size - 1} more)" else top
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.fabledsword.minstrel.models.wire
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One user's individual report under an aggregated quarantine row.
|
||||||
|
* Mirrors Flutter's `AdminQuarantineReport` /
|
||||||
|
* `internal/api/admin_quarantine.go` reportView.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class AdminQuarantineReportWire(
|
||||||
|
@SerialName("user_id") val userId: String = "",
|
||||||
|
val username: String = "",
|
||||||
|
val reason: String = "",
|
||||||
|
val notes: String? = null,
|
||||||
|
@SerialName("created_at") val createdAt: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row of `GET /api/admin/quarantine` — all reports against a
|
||||||
|
* single track collapsed into per-reason counts plus the underlying
|
||||||
|
* per-user reports. Mirrors `adminQueueRowView`.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class AdminQuarantineItemWire(
|
||||||
|
@SerialName("track_id") val trackId: String = "",
|
||||||
|
@SerialName("track_title") val trackTitle: String = "",
|
||||||
|
@SerialName("artist_name") val artistName: String = "",
|
||||||
|
@SerialName("album_title") val albumTitle: String = "",
|
||||||
|
@SerialName("album_id") val albumId: String = "",
|
||||||
|
@SerialName("lidarr_album_mbid") val lidarrAlbumMbid: String? = null,
|
||||||
|
@SerialName("report_count") val reportCount: Int = 0,
|
||||||
|
@SerialName("latest_at") val latestAt: String = "",
|
||||||
|
@SerialName("reason_counts") val reasonCounts: Map<String, Int> = emptyMap(),
|
||||||
|
val reports: List<AdminQuarantineReportWire> = emptyList(),
|
||||||
|
)
|
||||||
@@ -10,6 +10,7 @@ import androidx.navigation.NavHostController
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.fabledsword.minstrel.admin.ui.AdminQuarantineScreen
|
||||||
import com.fabledsword.minstrel.admin.ui.AdminRequestsScreen
|
import com.fabledsword.minstrel.admin.ui.AdminRequestsScreen
|
||||||
import com.fabledsword.minstrel.discover.ui.DiscoverScreen
|
import com.fabledsword.minstrel.discover.ui.DiscoverScreen
|
||||||
import com.fabledsword.minstrel.home.ui.HomeScreen
|
import com.fabledsword.minstrel.home.ui.HomeScreen
|
||||||
@@ -120,7 +121,7 @@ private fun NavGraphBuilder.inShellDetail(
|
|||||||
}
|
}
|
||||||
composable<AdminQuarantine> {
|
composable<AdminQuarantine> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(onExpandPlayer = expandPlayer) {
|
||||||
ComingSoon("Admin · Quarantine", "Lands with admin slice.")
|
AdminQuarantineScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<AdminUsers> {
|
composable<AdminUsers> {
|
||||||
|
|||||||
Reference in New Issue
Block a user