You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

480 lines
19 KiB

package org.calculate.taigamobile.ui.screens.main
import android.annotation.SuppressLint
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ScaffoldState
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.*
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.*
import com.google.accompanist.insets.*
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import org.calculate.taigamobile.R
import org.calculate.taigamobile.state.ThemeSetting
import org.calculate.taigamobile.domain.entities.CommonTaskType
import org.calculate.taigamobile.ui.components.containers.ContainerBox
import org.calculate.taigamobile.ui.components.appbars.AppBarWithBackButton
import org.calculate.taigamobile.ui.screens.login.LoginScreen
import org.calculate.taigamobile.ui.screens.projectselector.ProjectSelectorScreen
import org.calculate.taigamobile.ui.screens.scrum.ScrumScreen
import org.calculate.taigamobile.ui.screens.sprint.SprintScreen
import org.calculate.taigamobile.ui.screens.commontask.CommonTaskScreen
import org.calculate.taigamobile.ui.screens.createtask.CreateTaskScreen
import org.calculate.taigamobile.ui.screens.dashboard.DashboardScreen
import org.calculate.taigamobile.ui.screens.epics.EpicsScreen
import org.calculate.taigamobile.ui.screens.issues.IssuesScreen
import org.calculate.taigamobile.ui.screens.kanban.KanbanScreen
import org.calculate.taigamobile.ui.screens.profile.ProfileScreen
import org.calculate.taigamobile.ui.screens.settings.SettingsScreen
import org.calculate.taigamobile.ui.screens.team.TeamScreen
import org.calculate.taigamobile.ui.screens.wiki.createpage.WikiCreatePageScreen
import org.calculate.taigamobile.ui.screens.wiki.page.WikiPageScreen
import org.calculate.taigamobile.ui.screens.wiki.list.WikiListScreen
import org.calculate.taigamobile.ui.theme.TaigaMobileRippleTheme
import org.calculate.taigamobile.ui.theme.TaigaMobileTheme
import kotlinx.coroutines.launch
import java.io.InputStream
class MainActivity : AppCompatActivity() {
@SuppressLint("Range")
private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
it ?: return@registerForActivityResult
val inputStream = contentResolver.openInputStream(it) ?: return@registerForActivityResult
val fileName = contentResolver.query(it, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} ?: return@registerForActivityResult
filePicker.filePicked(fileName, inputStream)
}
private val filePicker: FilePicker = object : FilePicker() {
override fun requestFile(onFilePicked: (String, InputStream) -> Unit) {
super.requestFile(onFilePicked)
getContent.launch("*/*")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
val systemUiController = rememberSystemUiController()
val viewModel: MainViewModel = viewModel()
val theme by viewModel.theme.collectAsState()
val darkTheme = when (theme) {
ThemeSetting.Light -> false
ThemeSetting.Dark -> true
ThemeSetting.System -> isSystemInDarkTheme()
}
TaigaMobileTheme(darkTheme) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
systemUiController.let {
it.setStatusBarColor(
Color.Transparent,
darkIcons = !darkTheme
)
it.setNavigationBarColor(
Color.Transparent,
darkIcons = !darkTheme
)
}
CompositionLocalProvider(
LocalFilePicker provides filePicker,
LocalRippleTheme provides TaigaMobileRippleTheme
) {
// use Scaffold from material2, because material3 Scaffold lacks some functionality
androidx.compose.material.Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(
hostState = it,
modifier = Modifier.navigationBarsPadding()
) {
Snackbar(
snackbarData = it,
backgroundColor = MaterialTheme.colorScheme.surface,
contentColor = contentColorFor(MaterialTheme.colorScheme.surface),
shape = MaterialTheme.shapes.small
)
}
},
bottomBar = {
val items = Screens.values()
val routes = items.map { it.route }
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute =
navBackStackEntry?.destination?.hierarchy?.first()?.route
// hide bottom bar for other screens
if (currentRoute !in routes) return@Scaffold
NavigationBar(
modifier = Modifier.navigationBarsHeight(70.dp)
) {
items.forEach { screen ->
NavigationBarItem(
modifier = Modifier.navigationBarsPadding()
.clip(CircleShape),
icon = {
Icon(
painter = painterResource(screen.iconId),
contentDescription = null,
modifier = Modifier.size(22.dp)
)
},
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
if (screen.route != currentRoute) {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) { }
}
}
}
)
}
}
},
content = {
MainScreen(
viewModel = viewModel,
scaffoldState = scaffoldState,
paddingValues = it,
navController = navController
)
}
)
}
}
}
}
}
}
// Кнопки на главном экране
enum class Screens(val route: String, @StringRes val resourceId: Int, @DrawableRes val iconId: Int) {
Dashboard(Routes.dashboard, R.string.dashboard_short, R.drawable.ic_dashboard),
Epics(Routes.epics, R.string.epics, R.drawable.ic_epics),
Kanban(Routes.kanban, R.string.kanban, R.drawable.ic_kanban),
Team(Routes.team, R.string.team, R.drawable.ic_team),
//Settings(Routes.settings, R.string.settings, R.drawable.ic_settings)
//Scrum(Routes.scrum, R.string.scrum, R.drawable.ic_scrum),
//Issues(Routes.issues, R.string.issues, R.drawable.ic_issues),
More(Routes.more, R.string.more, R.drawable.ic_more)
}
object Routes {
const val login = "login"
const val dashboard = "dashboard"
const val scrum = "scrum"
const val epics = "epics"
const val issues = "issues"
const val more = "more"
const val team = "team"
const val settings = "settings"
const val kanban = "kanban"
const val wiki_selector = "wiki_selector"
const val wiki_page = "wiki_page"
const val wiki_create_page = "wiki_create_page"
const val projectsSelector = "projectsSelector"
const val sprint = "sprint"
const val commonTask = "commonTask"
const val createTask = "createTask"
const val profile = "profile"
object Arguments {
const val sprint = "sprint"
const val sprintId = "sprintId"
const val swimlaneId = "swimlaneId"
const val commonTaskId = "taskId"
const val commonTaskType = "taskType"
const val ref = "ref"
const val parentId = "parentId"
const val statusId = "statusId"
const val userId = "userId"
const val wikiSlug = "wikiSlug"
}
}
@Composable
fun MainScreen(
viewModel: MainViewModel,
scaffoldState: ScaffoldState,
paddingValues: PaddingValues,
navController: NavHostController
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val showMessage: (Int) -> Unit = { message ->
val strMessage = context.getString(message)
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(strMessage)
}
}
val isLogged by viewModel.isLogged.collectAsState()
val isProjectSelected by viewModel.isProjectSelected.collectAsState()
Surface(
modifier = Modifier.fillMaxSize().padding(paddingValues),
color = MaterialTheme.colorScheme.background
) {
NavHost(
navController = navController,
startDestination = remember { if (isLogged) Routes.dashboard else Routes.login }
) {
composable(Routes.login) {
LoginScreen(
navController = navController,
showMessage = showMessage
)
}
// start screen
composable(Routes.dashboard) {
DashboardScreen(
navController = navController,
showMessage = showMessage
)
// user must select project first
LaunchedEffect(Unit) {
if (!isProjectSelected) {
navController.navigate(Routes.projectsSelector)
}
}
}
composable(Routes.scrum) {
ScrumScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.epics) {
EpicsScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.issues) {
IssuesScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.more) {
MoreScreen(
navController = navController
)
}
composable(Routes.team) {
TeamScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.kanban) {
KanbanScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.wiki_selector) {
WikiListScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.wiki_create_page) {
WikiCreatePageScreen(
navController = navController,
showMessage = showMessage
)
}
composable(
"${Routes.wiki_page}/{${Routes.Arguments.wikiSlug}}",
arguments = listOf(
navArgument(Routes.Arguments.wikiSlug) { type = NavType.StringType }
)
) {
WikiPageScreen(
slug = it.arguments!!.getString(Routes.Arguments.wikiSlug).orEmpty(),
navController = navController,
showMessage = showMessage
)
}
composable(Routes.settings) {
SettingsScreen(
navController = navController,
showMessage = showMessage
)
}
composable(Routes.projectsSelector) {
ProjectSelectorScreen(
navController = navController,
showMessage = showMessage
)
}
composable(
"${Routes.sprint}/{${Routes.Arguments.sprintId}}",
arguments = listOf(
navArgument(Routes.Arguments.sprintId) { type = NavType.LongType }
)
) {
SprintScreen(
navController = navController,
sprintId = it.arguments!!.getLong(Routes.Arguments.sprintId),
showMessage = showMessage
)
}
composable(
"${Routes.profile}/{${Routes.Arguments.userId}}",
arguments = listOf(
navArgument(Routes.Arguments.userId) { type = NavType.LongType }
)
) {
ProfileScreen(
navController = navController,
showMessage = showMessage,
userId = it.arguments!!.getLong(Routes.Arguments.userId),
)
}
composable(
Routes.Arguments.run { "${Routes.commonTask}/{$commonTaskId}/{$commonTaskType}/{$ref}" },
arguments = listOf(
navArgument(Routes.Arguments.commonTaskType) { type = NavType.StringType },
navArgument(Routes.Arguments.commonTaskId) { type = NavType.LongType },
navArgument(Routes.Arguments.ref) { type = NavType.IntType },
)
) {
CommonTaskScreen(
navController = navController,
commonTaskId = it.arguments!!.getLong(Routes.Arguments.commonTaskId),
commonTaskType = CommonTaskType.valueOf(it.arguments!!.getString(Routes.Arguments.commonTaskType, "")),
ref = it.arguments!!.getInt(Routes.Arguments.ref),
showMessage = showMessage
)
}
composable(
Routes.Arguments.run {"${Routes.createTask}/{$commonTaskType}?$parentId={$parentId}&$sprintId={$sprintId}&$statusId={$statusId}&$swimlaneId={$swimlaneId}" },
arguments = listOf(
navArgument(Routes.Arguments.commonTaskType) { type = NavType.StringType },
navArgument(Routes.Arguments.parentId) {
type = NavType.LongType
defaultValue = -1L // long does not allow null values
},
navArgument(Routes.Arguments.sprintId) {
type = NavType.LongType
defaultValue = -1L
},
navArgument(Routes.Arguments.statusId) {
type = NavType.LongType
defaultValue = -1L
},
navArgument(Routes.Arguments.swimlaneId) {
type = NavType.LongType
defaultValue = -1L
},
)
) {
CreateTaskScreen(
navController = navController,
commonTaskType = CommonTaskType.valueOf(it.arguments!!.getString(Routes.Arguments.commonTaskType, "")),
parentId = it.arguments!!.getLong(Routes.Arguments.parentId).takeIf { it >= 0 },
sprintId = it.arguments!!.getLong(Routes.Arguments.sprintId).takeIf { it >= 0 },
statusId = it.arguments!!.getLong(Routes.Arguments.statusId).takeIf { it >= 0 },
swimlaneId = it.arguments!!.getLong(Routes.Arguments.swimlaneId).takeIf { it >= 0 },
showMessage = showMessage
)
}
}
}
}
@Composable
fun MoreScreen(
navController: NavController
) = Column(Modifier.fillMaxSize()) {
AppBarWithBackButton(
title = { Text(stringResource(R.string.more)) }
)
@Composable
fun Item(
@DrawableRes iconId: Int,
@StringRes nameId: Int,
route: String
) = ContainerBox(onClick = { navController.navigate(route) }) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(iconId),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.outline
)
Spacer(Modifier.width(8.dp))
Text(stringResource(nameId))
}
}
//val space = 2.dp
//Item(R.drawable.ic_team, R.string.team, Routes.team)
//Spacer(Modifier.height(space))
//Item(R.drawable.ic_kanban, R.string.kanban, Routes.kanban)
//Spacer(Modifier.height(space))
//Item(R.drawable.ic_wiki, R.string.wiki, Routes.wiki_selector)
//Spacer(Modifier.height(space))
Item(R.drawable.ic_settings, R.string.settings, Routes.settings)
}