ProfileFragment.kt 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. package ru.mephi.voip.ui.profile
  2. import android.os.Bundle
  3. import android.view.LayoutInflater
  4. import android.view.View
  5. import android.view.ViewGroup
  6. import android.widget.Toast
  7. import androidx.compose.foundation.Image
  8. import androidx.compose.foundation.background
  9. import androidx.compose.foundation.layout.*
  10. import androidx.compose.foundation.lazy.LazyColumn
  11. import androidx.compose.foundation.lazy.items
  12. import androidx.compose.foundation.shape.CircleShape
  13. import androidx.compose.foundation.text.KeyboardActions
  14. import androidx.compose.foundation.text.KeyboardOptions
  15. import androidx.compose.material.*
  16. import androidx.compose.material.icons.Icons
  17. import androidx.compose.material.icons.filled.Add
  18. import androidx.compose.material.icons.filled.CheckCircle
  19. import androidx.compose.material.icons.filled.Delete
  20. import androidx.compose.material.icons.filled.Edit
  21. import androidx.compose.runtime.*
  22. import androidx.compose.runtime.livedata.observeAsState
  23. import androidx.compose.ui.Alignment
  24. import androidx.compose.ui.ExperimentalComposeUiApi
  25. import androidx.compose.ui.Modifier
  26. import androidx.compose.ui.draw.clip
  27. import androidx.compose.ui.focus.FocusRequester
  28. import androidx.compose.ui.focus.focusRequester
  29. import androidx.compose.ui.graphics.Color
  30. import androidx.compose.ui.graphics.RectangleShape
  31. import androidx.compose.ui.layout.ContentScale
  32. import androidx.compose.ui.platform.ComposeView
  33. import androidx.compose.ui.platform.LocalContext
  34. import androidx.compose.ui.platform.LocalSoftwareKeyboardController
  35. import androidx.compose.ui.res.colorResource
  36. import androidx.compose.ui.res.dimensionResource
  37. import androidx.compose.ui.res.painterResource
  38. import androidx.compose.ui.res.stringResource
  39. import androidx.compose.ui.text.SpanStyle
  40. import androidx.compose.ui.text.buildAnnotatedString
  41. import androidx.compose.ui.text.font.FontWeight
  42. import androidx.compose.ui.text.input.ImeAction
  43. import androidx.compose.ui.text.input.KeyboardType
  44. import androidx.compose.ui.text.input.PasswordVisualTransformation
  45. import androidx.compose.ui.text.style.TextAlign
  46. import androidx.compose.ui.text.withStyle
  47. import androidx.compose.ui.unit.dp
  48. import androidx.compose.ui.unit.sp
  49. import androidx.fragment.app.Fragment
  50. import androidx.navigation.NavController
  51. import androidx.navigation.fragment.findNavController
  52. import coil.annotation.ExperimentalCoilApi
  53. import coil.compose.ImagePainter
  54. import coil.compose.rememberImagePainter
  55. import kotlinx.coroutines.launch
  56. import org.koin.android.ext.android.inject
  57. import ru.mephi.voip.R
  58. import ru.mephi.voip.call.abto.AccountStatus
  59. import ru.mephi.voip.data.model.Account
  60. import ru.mephi.voip.data.model.NameItem
  61. import ru.mephi.voip.ui.MainActivity
  62. @ExperimentalComposeUiApi
  63. @ExperimentalCoilApi
  64. @ExperimentalMaterialApi
  65. class ProfileFragment : Fragment() {
  66. private val viewModel: ProfileViewModel by inject()
  67. override fun onCreateView(
  68. inflater: LayoutInflater,
  69. container: ViewGroup?,
  70. savedInstanceState: Bundle?
  71. ): View {
  72. return ComposeView(requireContext()).apply {
  73. setContent {
  74. ProfileScreen(findNavController())
  75. }
  76. }
  77. }
  78. //https://dev.to/davidibrahim/how-to-use-multiple-bottom-sheets-in-android-compose-382p
  79. @ExperimentalMaterialApi
  80. @ExperimentalComposeUiApi
  81. @ExperimentalCoilApi
  82. @Composable
  83. fun ProfileScreen(navController: NavController) {
  84. val scope = rememberCoroutineScope()
  85. val scaffoldState = rememberBottomSheetScaffoldState()
  86. var currentBottomSheet: BottomSheetScreen? by remember {
  87. mutableStateOf(null)
  88. }
  89. if (scaffoldState.bottomSheetState.isCollapsed)
  90. currentBottomSheet = null
  91. // to set the current sheet to null when the bottom sheet closes
  92. if (scaffoldState.bottomSheetState.isCollapsed)
  93. currentBottomSheet = null
  94. val closeSheet: () -> Unit = {
  95. scope.launch {
  96. scaffoldState.bottomSheetState.collapse()
  97. }
  98. }
  99. val openSheet: (BottomSheetScreen) -> Unit = {
  100. scope.launch {
  101. currentBottomSheet = it
  102. scaffoldState.bottomSheetState.expand()
  103. }
  104. }
  105. BottomSheetScaffold(
  106. sheetElevation = 20.dp,
  107. sheetPeekHeight = 0.dp, scaffoldState = scaffoldState,
  108. sheetShape = BottomSheetShape,
  109. sheetContent = {
  110. currentBottomSheet?.let { currentSheet ->
  111. SheetLayout(currentSheet, closeSheet, scaffoldState)
  112. }
  113. }) { paddingValues ->
  114. Box(Modifier.padding(paddingValues)) {
  115. MainContent(openSheet, navController)
  116. }
  117. }
  118. }
  119. @ExperimentalMaterialApi
  120. @ExperimentalComposeUiApi
  121. @Composable
  122. fun SheetLayout(
  123. currentScreen: BottomSheetScreen,
  124. onCloseBottomSheet: () -> Unit,
  125. scaffoldState: BottomSheetScaffoldState
  126. ) {
  127. BottomSheetWithCloseDialog(
  128. onCloseBottomSheet, title =
  129. when (currentScreen) {
  130. BottomSheetScreen.ScreenAddNewAccount -> stringResource(id = R.string.add_new_account)
  131. BottomSheetScreen.ScreenChangeAccount -> stringResource(id = R.string.change_account)
  132. else -> ""
  133. }
  134. ) {
  135. when (currentScreen) {
  136. BottomSheetScreen.ScreenAddNewAccount -> ScreenAddNewAccount(scaffoldState)
  137. BottomSheetScreen.ScreenChangeAccount -> ScreenChangeAccount()
  138. }
  139. }
  140. }
  141. @ExperimentalMaterialApi
  142. @ExperimentalComposeUiApi
  143. @Composable
  144. fun ScreenAddNewAccount(scaffoldState: BottomSheetScaffoldState) {
  145. var textLogin = viewModel.newLogin.value
  146. var textPassword = viewModel.newPassword.value
  147. val context = LocalContext.current
  148. val maxNumberLength = 6
  149. val (focusRequester) = FocusRequester.createRefs()
  150. val keyboardController = LocalSoftwareKeyboardController.current
  151. val scope = rememberCoroutineScope()
  152. Column(
  153. modifier = Modifier
  154. .fillMaxWidth()
  155. .background(Color.White, shape = RectangleShape)
  156. .height(400.dp),
  157. horizontalAlignment = Alignment.CenterHorizontally
  158. ) {
  159. OutlinedTextField(
  160. singleLine = true,
  161. keyboardOptions = KeyboardOptions(
  162. keyboardType = KeyboardType.Number,
  163. imeAction = ImeAction.Next
  164. ),
  165. keyboardActions = KeyboardActions(
  166. onNext = { focusRequester.requestFocus() }
  167. ),
  168. colors = TextFieldDefaults.outlinedTextFieldColors(
  169. focusedBorderColor = colorResource(id = R.color.colorPrimaryDark),
  170. focusedLabelColor = colorResource(id = R.color.colorAccent),
  171. cursorColor = colorResource(id = R.color.colorAccent),
  172. ),
  173. modifier = Modifier
  174. .fillMaxWidth()
  175. .padding(16.dp),
  176. label = { Text("SIP USER ID") },
  177. value = textLogin,
  178. onValueChange = {
  179. if (it.length <= maxNumberLength) {
  180. textLogin = it
  181. viewModel.onNewAccountInputChange(login = it)
  182. } else
  183. Toast.makeText(
  184. context,
  185. "Номер не может быть больше $maxNumberLength символов",
  186. Toast.LENGTH_SHORT
  187. ).show()
  188. }
  189. )
  190. OutlinedTextField(
  191. singleLine = true,
  192. colors = TextFieldDefaults.outlinedTextFieldColors(
  193. focusedBorderColor = colorResource(id = R.color.colorPrimaryDark),
  194. focusedLabelColor = colorResource(id = R.color.colorAccent),
  195. cursorColor = colorResource(id = R.color.colorAccent),
  196. ),
  197. enabled = false,
  198. modifier = Modifier
  199. .fillMaxWidth()
  200. .padding(16.dp),
  201. label = { Text("SIP Server") },
  202. value = stringResource(id = R.string.domain),
  203. onValueChange = { }
  204. )
  205. OutlinedTextField(
  206. singleLine = true,
  207. colors = TextFieldDefaults.outlinedTextFieldColors(
  208. focusedBorderColor = colorResource(id = R.color.colorPrimaryDark),
  209. focusedLabelColor = colorResource(id = R.color.colorAccent),
  210. cursorColor = colorResource(id = R.color.colorAccent),
  211. ),
  212. keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
  213. keyboardActions = KeyboardActions(
  214. onDone = { keyboardController?.hide() }
  215. ),
  216. visualTransformation = PasswordVisualTransformation(),
  217. modifier = Modifier
  218. .focusRequester(focusRequester)
  219. .fillMaxWidth()
  220. .padding(16.dp),
  221. label = { Text("SIP PASSWORD") },
  222. value = textPassword,
  223. onValueChange = {
  224. viewModel.onNewAccountInputChange(password = it)
  225. }
  226. )
  227. OutlinedButton(onClick = {
  228. if (textLogin.toIntOrNull() == null || textPassword.isEmpty()) {
  229. Toast.makeText(context, "Введены некоректные данные", Toast.LENGTH_SHORT).show()
  230. } else if (viewModel.getAllAccounts().map { it.login }.contains(textLogin)) {
  231. Toast.makeText(context, "Такой аккаунт уже существует", Toast.LENGTH_SHORT)
  232. .show()
  233. } else {
  234. viewModel.addNewAccount()
  235. scope.launch {
  236. scaffoldState.bottomSheetState.collapse()
  237. }
  238. }
  239. viewModel.newLogin.value = ""
  240. viewModel.newPassword.value = ""
  241. }) {
  242. Text("Добавить", color = colorResource(id = R.color.colorPrimaryDark))
  243. }
  244. }
  245. }
  246. @Composable
  247. fun ScreenChangeAccount() {
  248. val mList: MutableList<Account> by remember { mutableStateOf(viewModel.getAllAccounts()) }
  249. viewModel.accountList.observe(viewLifecycleOwner) {
  250. mList.apply {
  251. clear()
  252. addAll(viewModel.getAllAccounts())
  253. }
  254. }
  255. Box(
  256. modifier = Modifier
  257. .fillMaxWidth()
  258. .height(200.dp)
  259. .background(Color.White, shape = RectangleShape)
  260. ) {
  261. LazyColumn {
  262. items(items = mList) { acc ->
  263. AccountItem(acc)
  264. }
  265. }
  266. }
  267. }
  268. @Composable
  269. fun AccountItem(account: Account) {
  270. val context = LocalContext.current
  271. Card(
  272. shape = MaterialTheme.shapes.medium, elevation = 2.dp,
  273. modifier = Modifier
  274. .padding(vertical = 4.dp, horizontal = 20.dp)
  275. .fillMaxWidth()
  276. .height(50.dp)
  277. ) {
  278. Box {
  279. IconButton(
  280. onClick = {
  281. if ((context as MainActivity).hasPermissions()) {
  282. viewModel.updateActiveAccount(account)
  283. // Toast.makeText(
  284. // context,
  285. // "Активный аккаунт: ${viewModel.newLogin.value}",
  286. // Toast.LENGTH_SHORT
  287. // ).show()
  288. } else
  289. context.requestPermissions()
  290. // viewModel.clear()
  291. // mList.addAll(viewModel.getAllAccounts())
  292. }, modifier = Modifier
  293. .align(Alignment.CenterStart)
  294. .padding(10.dp)
  295. ) {
  296. Icon(
  297. Icons.Filled.CheckCircle, "Status",
  298. tint = if (account.isActive)
  299. colorResource(id = R.color.colorAccent)
  300. else
  301. colorResource(id = R.color.colorGray),
  302. )
  303. }
  304. Text(
  305. text = account.login,
  306. modifier = Modifier
  307. .padding(6.dp)
  308. .align(Alignment.Center),
  309. fontSize = 20.sp,
  310. fontWeight = FontWeight.Medium,
  311. )
  312. IconButton(
  313. onClick = {
  314. viewModel.removeAccount(account)
  315. }, modifier = Modifier
  316. .align(Alignment.CenterEnd)
  317. .padding(10.dp)
  318. ) {
  319. Icon(
  320. Icons.Filled.Delete, "Delete",
  321. tint = Color.Red
  322. )
  323. }
  324. }
  325. }
  326. }
  327. sealed class BottomSheetScreen {
  328. object ScreenAddNewAccount : BottomSheetScreen()
  329. object ScreenChangeAccount : BottomSheetScreen()
  330. }
  331. @ExperimentalCoilApi
  332. @Composable
  333. fun MainContent(
  334. openSheet: (BottomSheetScreen) -> Unit,
  335. navController: NavController
  336. ) {
  337. val painter = rememberImagePainter(
  338. data = viewModel.getImageUrl(),
  339. builder = {
  340. crossfade(true)
  341. }
  342. )
  343. val name = viewModel.displayName.observeAsState(NameItem("", ""))
  344. val accountStatus = viewModel.status.observeAsState(AccountStatus.UNREGISTERED)
  345. Column(
  346. Modifier.fillMaxWidth(),
  347. horizontalAlignment = Alignment.CenterHorizontally,
  348. ) {
  349. Box(
  350. modifier = Modifier
  351. .fillMaxWidth()
  352. .padding(15.dp)
  353. ) {
  354. Text(
  355. modifier = Modifier.align(alignment = Alignment.TopCenter),
  356. text = stringResource(R.string.sip_account_header),
  357. fontSize = dimensionResource(id = R.dimen.toolbar_headers).value.sp,
  358. )
  359. IconButton(
  360. modifier = Modifier.align(alignment = Alignment.TopEnd),
  361. onClick = {
  362. navController.navigate(
  363. R.id.action_navigation_profile_to_settingsFragment,
  364. )
  365. }) {
  366. Icon(
  367. painter = painterResource(id = R.drawable.ic_baseline_settings_24),
  368. contentDescription = "Настройки"
  369. )
  370. }
  371. }
  372. Row(
  373. horizontalArrangement = Arrangement.SpaceBetween,
  374. verticalAlignment = Alignment.CenterVertically
  375. ) {
  376. Image(
  377. painter = painterResource(id = R.drawable.logo_mephi),
  378. contentDescription = "лого"
  379. )
  380. Box(modifier = Modifier.size(100.dp)) {
  381. Image(
  382. painter = painter,
  383. contentDescription = "",
  384. contentScale = ContentScale.Crop,
  385. modifier = Modifier
  386. .clip(CircleShape)
  387. .fillMaxSize()
  388. .align(Alignment.BottomEnd)
  389. )
  390. Icon(
  391. Icons.Filled.CheckCircle, "Статус",
  392. tint = when (accountStatus.value) {
  393. AccountStatus.REGISTERED -> colorResource(id = R.color.colorGreen)
  394. AccountStatus.UNREGISTERED, AccountStatus.REGISTRATION_FAILED -> Color.Red
  395. AccountStatus.NO_CONNECTION, AccountStatus.CHANGING, AccountStatus.LOADING -> Color.Gray
  396. },
  397. modifier = Modifier
  398. .align(Alignment.BottomEnd)
  399. )
  400. when (painter.state) {
  401. is ImagePainter.State.Loading -> {
  402. CircularProgressIndicator(Modifier.align(Alignment.Center))
  403. }
  404. is ImagePainter.State.Error -> {
  405. // If you wish to display some content if the request fails
  406. }
  407. }
  408. }
  409. if (accountStatus.value == AccountStatus.REGISTRATION_FAILED
  410. || accountStatus.value == AccountStatus.UNREGISTERED
  411. )
  412. IconButton(
  413. modifier = Modifier
  414. .align(Alignment.Bottom),
  415. onClick = {
  416. viewModel.retryRegistration()
  417. }
  418. ) {
  419. Icon(
  420. painter = painterResource(
  421. id = R.drawable.ic_baseline_update_24
  422. ),
  423. contentDescription = "Обновить",
  424. tint = Color.Red
  425. )
  426. }
  427. }
  428. Column(
  429. Modifier
  430. .fillMaxWidth()
  431. .padding(20.dp),
  432. horizontalAlignment = Alignment.Start
  433. ) {
  434. if (!name.value?.display_name.isNullOrEmpty())
  435. Text(
  436. fontSize = 25.sp,
  437. fontWeight = FontWeight.Medium,
  438. textAlign = TextAlign.Left,
  439. text = buildAnnotatedString {
  440. withStyle(style = SpanStyle(color = colorResource(id = R.color.colorAccent))) {
  441. append("Имя: ")
  442. }
  443. append(name.value!!.display_name)
  444. }
  445. )
  446. viewModel.getUserNumber()?.let {
  447. Text(
  448. fontSize = 25.sp,
  449. fontWeight = FontWeight.Medium,
  450. textAlign = TextAlign.Left,
  451. text = buildAnnotatedString {
  452. withStyle(style = SpanStyle(color = colorResource(id = R.color.colorAccent))) {
  453. append("Номер SIP: ")
  454. }
  455. append(it)
  456. }
  457. )
  458. }
  459. Text(
  460. fontSize = 25.sp,
  461. fontWeight = FontWeight.Medium,
  462. textAlign = TextAlign.Left,
  463. text = buildAnnotatedString {
  464. withStyle(style = SpanStyle(color = colorResource(id = R.color.colorAccent))) {
  465. append("Статус: ")
  466. }
  467. append(accountStatus.value.status)
  468. }
  469. )
  470. }
  471. Column(
  472. modifier = Modifier
  473. .fillMaxSize(), verticalArrangement = Arrangement.Bottom
  474. ) {
  475. ExtendedFloatingActionButton(
  476. icon = { Icon(Icons.Filled.Edit, "", tint = Color.White) },
  477. text = {
  478. Text(
  479. text = stringResource(R.string.change_account) + " (${viewModel.getAllAccounts().size})",
  480. color = Color.White
  481. )
  482. },
  483. modifier = Modifier
  484. .align(Alignment.End)
  485. .padding(16.dp, 16.dp, 16.dp, 0.dp),
  486. backgroundColor = colorResource(id = R.color.colorGreen),
  487. onClick = {
  488. openSheet(BottomSheetScreen.ScreenChangeAccount)
  489. // navController.navigate(
  490. // R.id.action_navigation_profile_to_settingsFragment,
  491. // )
  492. }
  493. )
  494. ExtendedFloatingActionButton(
  495. icon = { Icon(Icons.Filled.Add, "", tint = Color.White) },
  496. text = {
  497. Text(
  498. text = stringResource(R.string.add_new_account),
  499. color = Color.White
  500. )
  501. },
  502. backgroundColor = colorResource(id = R.color.colorGreen),
  503. modifier = Modifier
  504. .align(Alignment.End)
  505. .padding(16.dp, 16.dp, 16.dp, 16.dp),
  506. onClick = {
  507. openSheet(BottomSheetScreen.ScreenAddNewAccount)
  508. // navController.navigate(
  509. // R.id.action_navigation_profile_to_fragmentAdd,
  510. // )
  511. }
  512. )
  513. }
  514. }
  515. }
  516. }