archive-97f2350/app/src/main/java/uk/orllewin/sianel/ui/SianelRootScreen.kt 8.0 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
package uk.orllewin.sianel.ui

import android.content.res.Configuration
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.ui.NavDisplay
import uk.orllewin.sianel.R

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SianelRootScreen(
    externalUri: android.net.Uri? = null,
    onExternalUriConsumed: () -> Unit = {},
) {
    val viewModel: MainViewModel = viewModel()
    var showConfigDialog by remember { mutableStateOf(false) }
    var menuOpen by remember { mutableStateOf(false) }

    // First run: no credentials yet. Show the full-screen setup instead of the main
    // UI (which would otherwise render an error behind a bottom sheet).
    if (viewModel.password.isBlank()) {
        ConfigScreen(
            currentPassword = viewModel.password,
            currentBaseUrl = viewModel.baseUrl,
            verifyState = viewModel.verifyState,
            onCheck = { password, baseUrl -> viewModel.verifyCredentials(password, baseUrl) },
            onSave = { password, baseUrl -> viewModel.saveSettings(password, baseUrl) },
        )
        return
    }

    val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
    // containerSize is in pixels; the 600 tablet breakpoint is in dp — convert before
    // comparing, otherwise every phone (~1080px wide) is wrongly treated as a tablet.
    val isTablet = with(LocalDensity.current) {
        LocalWindowInfo.current.containerSize.width.toDp() >= 600.dp
    }

    val backStack = remember { mutableStateListOf<Any>(ListRoute) }

    RequestNotificationPermissionOnce()

    // Stage videos opened from external apps (Photos share, "Open with", etc.).
    LaunchedEffect(externalUri) {
        val uri = externalUri ?: return@LaunchedEffect
        viewModel.selectVideo(uri)
        if (!isTablet && backStack.lastOrNull() !is UploadRoute) {
            backStack.add(UploadRoute)
        }
        onExternalUriConsumed()
    }

    val picker = rememberLauncherForActivityResult(
        ActivityResultContracts.GetContent()
    ) { uri ->
        uri?.let {
            viewModel.selectVideo(it)
            if (!isTablet && backStack.lastOrNull() !is UploadRoute) {
                backStack.add(UploadRoute)
            }
        }
    }
    val pickVideo: () -> Unit = { picker.launch("video/*") }

    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            TopAppBar(
                scrollBehavior = scrollBehavior,
                title = {
                    val logoRes = if (isSystemInDarkTheme()) {
                        R.drawable.screen_logo_dark
                    } else {
                        R.drawable.screen_logo
                    }
                    Image(
                        modifier = Modifier.height(40.dp),
                        painter = painterResource(id = logoRes),
                        contentDescription = "Sianel",
                    )
                },
                actions = {
                    IconButton(onClick = { menuOpen = true }) {
                        Icon(Icons.Default.MoreVert, contentDescription = "More")
                    }
                    DropdownMenu(
                        expanded = menuOpen,
                        onDismissRequest = { menuOpen = false },
                    ) {
                        DropdownMenuItem(
                            text = { Text("Config") },
                            onClick = {
                                menuOpen = false
                                showConfigDialog = true
                            },
                        )
                    }
                },
            )
        },
        floatingActionButton = {
            val onList = backStack.lastOrNull() is ListRoute
            if (!isTablet &&
                !isLandscape &&
                onList &&
                viewModel.password.isNotBlank() &&
                viewModel.uploadProgress == null
            ) {
                FloatingActionButton(
                    onClick = pickVideo,
                    shape = CircleShape
                ) {
                    Icon(
                        painter = painterResource(R.drawable.ic_add_video),
                        contentDescription = "Add"
                    )
                }
            }
        },
    ) { padding ->
        if (isTablet && isLandscape) {
            TwoPaneLayout(
                viewModel = viewModel,
                onPickVideo = pickVideo,
                paddingValues = padding,
            )
        } else {
            NavDisplay(
                backStack = backStack,
                onBack = {
                    if (backStack.lastOrNull() is UploadRoute) viewModel.cancelPending()
                    if (backStack.size > 1) backStack.removeLastOrNull()
                },
                entryProvider = { key ->
                    when (key) {
                        is ListRoute -> NavEntry(key) {
                            ListScreen(viewModel, padding)
                        }
                        is UploadRoute -> NavEntry(key) {
                            UploadScreen(
                                viewModel = viewModel,
                                paddingValues = padding,
                                onUpload = {
                                    viewModel.confirmUpload()
                                    if (backStack.size > 1) backStack.removeLastOrNull()
                                },
                                onCancel = {
                                    viewModel.cancelPending()
                                    if (backStack.size > 1) backStack.removeLastOrNull()
                                },
                            )
                        }
                        else -> NavEntry(key) { Text("Unknown route") }
                    }
                },
            )
        }
    }

    if (showConfigDialog) {
        ConfigDialog(
            currentPassword = viewModel.password,
            currentBaseUrl = viewModel.baseUrl,
            onSave = { password, baseUrl ->
                viewModel.saveSettings(password, baseUrl)
                showConfigDialog = false
            },
            onClear = {
                viewModel.clearPassword()
                viewModel.clearBaseUrl()
                showConfigDialog = false
            },
            onDismiss = { showConfigDialog = false },
        )
    }
}