Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • some-cleanup
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.0.3
  • v1.1.0
  • v1.1.1
  • v1.2.0
  • v1.2.2
  • v1.2.3
  • v1.2.4
12 results

Target

Select target project
  • jonas.broeckmann/kaffeekasse
1 result
Select Git revision
  • master
  • some-cleanup
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.0.3
  • v1.1.0
  • v1.1.1
  • v1.2.0
  • v1.2.2
  • v1.2.3
  • v1.2.4
12 results
Show changes
Commits on Source (5)
Showing
with 440 additions and 202 deletions
......@@ -32,6 +32,7 @@ val KnownItem.category get() = when (this) {
CocaCola330 -> ItemCategory.ColdBeverage
CokeZero -> ItemCategory.ColdBeverage
CokeZeroCaffeineFree -> ItemCategory.ColdBeverage
FritzCola -> ItemCategory.ColdBeverage
ChouffeBier -> ItemCategory.ColdBeverage
EiflerLandbier -> ItemCategory.ColdBeverage
EngelbertApple -> ItemCategory.ColdBeverage
......@@ -66,6 +67,8 @@ val KnownItem.category get() = when (this) {
Milka -> ItemCategory.Snack
Balisto -> ItemCategory.Snack
Duplo -> ItemCategory.Snack
PickUp -> ItemCategory.Snack
LindtHase -> ItemCategory.Snack
OreoCookies -> ItemCategory.Snack
Trolli -> ItemCategory.Snack
Twix -> ItemCategory.Snack
......@@ -110,6 +113,7 @@ val KnownItem.cleanProductName get() = when (this) {
CocaCola05Glass -> "Coca-Cola"
CokeZero -> "Coca-Cola Zero"
CokeZeroCaffeineFree -> "Coca-Cola Zero"
FritzCola -> "Fritz Cola"
EngelbertApple -> "Engelbert Apfel"
EngelbertNatural -> "Engelbert Naturell"
EngelbertSprudel -> "Engelbert Sprudel"
......@@ -138,6 +142,8 @@ val KnownItem.cleanProductName get() = when (this) {
Milka -> "Milka"
Balisto -> "Balisto"
Duplo -> "Duplo"
PickUp -> "PickUp"
LindtHase -> "Lindt"
OreoCookies -> "Oreo"
Trolli -> "Trolli"
Twix -> "Twix"
......@@ -184,6 +190,7 @@ val KnownItem.cleanVariantName get() = when (this) {
CoffeeLarge -> "groß"
HariboBag -> "Beutel"
Katjes -> "Beutel"
LindtHase -> "Hase"
DuplexColor -> "Farbe"
DuplexBlackWhite -> "S/W"
SimplexColor -> "Farbe"
......@@ -193,72 +200,75 @@ val KnownItem.cleanVariantName get() = when (this) {
}
val KnownItem.estimatedPrice get() = when (this) {
Almdudler -> null
Bier330 -> null
Bier330 -> 1.06
Bier500 -> 1.25
Bionade -> 0.82
ChouffeBier -> null
ChouffeBier -> 1.80
ClubMate -> null
CocaCola -> null
CocaCola330 -> null
CocaCola05Glass -> null
CocaCola -> 1.73
CocaCola330 -> 1.22
CocaCola05Glass -> 1.35
CokeZero -> 1.66
CokeZeroCaffeineFree -> null
FritzCola -> 1.57
CafeAuLait -> 0.33
Cappuccino -> 0.42
CoffeeSmall -> null
CoffeeSmall -> 0.19
CoffeeLarge -> 0.32
ChocolateCreamCocoa -> 0.41
ErdingerAlcoholFree330 -> null
ErdingerAlcoholFree500 -> null
EiflerLandbier -> null
EngelbertApple -> null
EngelbertNatural -> null
EngelbertSprudel -> null
ErdingerAlcoholFree330 -> 1.15
ErdingerAlcoholFree500 -> 1.45
EiflerLandbier -> 1.15
EngelbertApple -> 0.98
EngelbertNatural -> 0.57
EngelbertSprudel -> 0.57
Espresso -> 0.19
Erdnuesse -> 1.30
Fanta -> 1.55
Fassbrause -> 0.82
Gerolsteiner -> 0.93
Leffe -> null
Leffe -> 2.00
LatteMacchiato -> 0.55
Monster -> null
Monster -> 1.69
MilkMachine -> 0.25
MioMioMate -> 1.03
Moccachoc -> 0.55
Rockstar -> null
RheinfelsSprudel -> null
Rockstar -> 1.69
RheinfelsSprudel -> 0.57
Hanuta -> 0.28
HariboBag -> 1.15
Knoppers -> 0.32
KnoppersBar -> 0.45
Milka -> null
Milka -> 0.99
Balisto -> 0.25
Duplo -> 0.26
PickUp -> 0.34
LindtHase -> 1.00
OreoCookies -> 0.40
Trolli -> 0.99
Twix -> null
Yogurette -> 0.19
KinderChocolate -> null
NutsRoyalNuts -> 2.25
CujaMaraSplit -> null
CujaMaraSplit -> 0.65
Magnum -> 0.99
NUII -> 0.99
IceCornetto -> 0.50
IceNogger -> null
IceMars -> null
IceSnickers -> null
IceTwix -> null
IceBounty -> null
IceNogger -> 0.65
IceMars -> 0.68
IceSnickers -> 0.68
IceTwix -> 0.68
IceBounty -> 0.68
Pizza -> 2.99
Apple -> null
Apple -> 0.30
Tangerine -> null
Clementine -> null
DuplexColor -> 0.05
DuplexBlackWhite -> null
SimplexColor -> null
SimplexBlackWhite -> null
Tissues -> null
ThreeDPrintingPerGram -> null
DuplexBlackWhite -> 0.03
SimplexColor -> 0.03
SimplexBlackWhite -> 0.02
Tissues -> 0.12
ThreeDPrintingPerGram -> 0.02
Euglueh -> null
Katjes -> 0.99
}
......
......@@ -18,6 +18,7 @@ enum class KnownItem(
CocaCola330 (266, "Coca Cola 0,33l"),
CokeZero ( 78, "Coke ZERO"),
CokeZeroCaffeineFree (246, "Coke Zero koffeinfrei"),
FritzCola (186, "Fritz Cola"),
DuplexColor (155, "Duplex Farbe"),
ChouffeBier (262, "Chouffe Bier"),
DuplexBlackWhite ( 91, "Duplex S/W"),
......@@ -57,6 +58,8 @@ enum class KnownItem(
Apple (195, "Apfel"),
Balisto (105, "Balisto"),
Duplo (231, "Duplo"),
PickUp (106, "PickUp!"),
LindtHase (267, "Lindt Hase"),
IceCornetto (128, "Eis: Cornetto"),
IceNogger (101, "Eis: Nogger"),
IceMars (136, "Eis: Mars/Snickers/Twix/Bounty"),
......
......@@ -16,7 +16,6 @@ import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
import net.novagamestudios.kaffeekasse.model.kaffeekasse.isNotEmpty
import net.novagamestudios.kaffeekasse.model.session.Session
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.ui.AppModuleSelection
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.ManualBillScreenModel.Companion.backNavigationHandler
import net.novagamestudios.kaffeekasse.ui.navigation.AppSubpageTitle
import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation
......@@ -34,7 +33,6 @@ class KaffeekasseModuleScreenModel private constructor(
else -> null
}
companion object : ScreenModelFactory<KaffeekasseNavigation.Tab, KaffeekasseModuleScreenModel> {
context (RepositoryProvider)
override fun create(screen: KaffeekasseNavigation.Tab) = KaffeekasseModuleScreenModel(
......@@ -52,12 +50,8 @@ fun KaffeekasseTopBarTitle(
model: KaffeekasseModuleScreenModel,
navigator: Navigator
) {
when (navigator.lastItem) {
is KaffeekasseNavigation.ManualBillScreen -> {
val name = model.accountName
if (name != null) AppSubpageTitle("$name")
else AppModuleSelection()
}
when (val screen = navigator.lastItem) {
is KaffeekasseNavigation.ManualBillScreen -> ManualBillTopBarTitle(screen.model, navigator)
is KaffeekasseNavigation.AccountScreen -> AppSubpageTitle("Konto")
is KaffeekasseNavigation.TransactionsScreen -> AppSubpageTitle("Übersicht")
}
......
......@@ -40,14 +40,17 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.compose.components.CircularProgressIndicator
import net.novagamestudios.common_utils.compose.components.RowCenter
import net.novagamestudios.common_utils.compose.tabIndicatorOffset
import net.novagamestudios.kaffeekasse.KaffeekasseModule.Companion.kaffeekasseCartProvider
import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item
import net.novagamestudios.kaffeekasse.model.kaffeekasse.MutableCart
import net.novagamestudios.kaffeekasse.model.kaffeekasse.isEmpty
import net.novagamestudios.kaffeekasse.model.session.Session
import net.novagamestudios.kaffeekasse.model.session.deviceOrNull
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
import net.novagamestudios.kaffeekasse.ui.AppModuleSelection
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CategorizedItems
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CategorizedItemsState
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.Checkout
......@@ -55,9 +58,13 @@ import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CheckoutState
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItems
import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItemsState
import net.novagamestudios.kaffeekasse.ui.login.LogoutTopBarAction
import net.novagamestudios.kaffeekasse.ui.navigation.AppSubpageTitle
import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation
import net.novagamestudios.kaffeekasse.ui.util.FailureRetryScreen
import net.novagamestudios.kaffeekasse.ui.util.RichDataContent
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchAction
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchField
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchFieldState
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler.Companion.then
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
......@@ -70,13 +77,44 @@ class ManualBillScreenModel private constructor(
private val kaffeekasse: KaffeekasseRepository,
val cart: MutableCart
) : ScreenModel, Logger {
val accountName = when (session) {
is Session.WithDevice -> session.realUser.displayName ?: "Unknown User"
else -> null
}
// private var _showSearch by mutableStateOf(false)
// var showSearch
// get() = _showSearch
// set(value) {
// _showSearch = value
// if (!value) searchQuery = ""
// }
// var searchQuery by mutableStateOf("")
val searchState = TopBarSearchFieldState()
val stock = kaffeekasse.stock.collectAsRichStateHere()
val itemGroups get() = stock.dataOrNull?.itemGroups ?: emptyList()
fun Sequence<Item>.filtered(): Sequence<Item> {
val query = searchState.queryOrNull
return if (query != null) filter { item ->
item.cleanFullName.contains(query, ignoreCase = true)
} else this
}
val searchItems by derivedStateOf {
if (searchState.show) CategorizedItemsState(
itemGroups
.asSequence()
.flatMap { it.items }
.filtered()
.toList()
) else null
}
val categorizedItemsByGroup by derivedStateOf {
itemGroups.map { CategorizedItemsState(it.items) }
itemGroups.map { group -> CategorizedItemsState(group.items.asSequence().filtered().toList()) }
}
val customItemsState by lazy {
......@@ -127,13 +165,18 @@ class ManualBillScreenModel private constructor(
@Composable
fun ManualBillScreenModel.backNavigationHandler(navigator: Navigator): BackNavigationHandler {
val categorizedItems = categorizedItemsByGroup.getOrNull(currentGroupIndex)
val categorizedItems = if (currentPagerIndex == 0) {
searchItems
} else {
categorizedItemsByGroup.getOrNull(currentGroupIndex)
}
return remember(categorizedItems, cart) {
categorizedItems?.backNavigation then CartBackNavigationHandler(cart)
categorizedItems?.backNavigation then searchState.backNavigation then CartBackNavigationHandler(cart)
}
}
}
private class CartBackNavigationHandler(
private val cart: MutableCart
) : BackNavigationHandler {
......@@ -238,28 +281,73 @@ private fun TabbedItemGroups(
}
}
HorizontalDivider()
HorizontalPager(model.pagerState) { index ->
if (index == 0) CustomItems(
state = model.customItemsState,
cart = model.cart,
Modifier
.weight(1f)
.fillMaxHeight()
) else CategorizedItems(
state = model.categorizedItemsByGroup[index - 1],
cart = model.cart,
Modifier
.weight(1f)
.fillMaxHeight()
HorizontalPager(
model.pagerState,
beyondViewportPageCount = 4
) { index ->
if (index == 0) CustomItemsTab(
model = model,
Modifier.fillMaxHeight()
) else ItemGroupTab(
model = model,
groupIndex = index - 1,
Modifier.fillMaxHeight()
)
}
}
@Composable
private fun CustomItemsTab(
model: ManualBillScreenModel,
modifier: Modifier = Modifier
) {
val state = model.searchItems
if (state != null) {
CategorizedItems(
state = state,
cart = model.cart,
modifier
)
} else CustomItems(
state = model.customItemsState,
cart = model.cart,
modifier
)
}
@Composable
private fun ItemGroupTab(
model: ManualBillScreenModel,
groupIndex: Int,
modifier: Modifier = Modifier
) {
CategorizedItems(
state = model.categorizedItemsByGroup[groupIndex],
cart = model.cart,
modifier
)
}
@Composable
fun ManualBillTopBarTitle(
model: ManualBillScreenModel,
navigator: Navigator
) = RowCenter {
if (!model.searchState.show) {
val name = model.accountName
if (name != null) AppSubpageTitle("$name")
else AppModuleSelection()
}
TopBarSearchField(model.searchState)
}
@Composable
fun ManualBillTopBarActions(
model: ManualBillScreenModel
) {
TopBarSearchAction(model.searchState)
val navigator = LocalNavigator.currentOrThrow
IconButton(onClick = {
navigator.push(KaffeekasseNavigation.AccountScreen(model.session))
......
......@@ -77,7 +77,6 @@ import net.novagamestudios.kaffeekasse.util.richdata.RichDataState
import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichStateIn
import net.novagamestudios.kaffeekasse.util.richdata.combineRich
import net.novagamestudios.kaffeekasse.util.richdata.dataOrNull
import net.novagamestudios.kaffeekasse.util.richdata.flatMapLatestRich
import net.novagamestudios.kaffeekasse.util.richdata.mapRich
import net.novagamestudios.kaffeekasse.util.richdata.stateIn
import kotlin.time.Duration.Companion.seconds
......@@ -229,9 +228,9 @@ private class APIPurchaseController(
}.stateIn(coroutineScope, SharingStarted.Eagerly)
private val selfId get() = selfUserBasic.value.dataOrNull?.id
private val selfUserExtended = selfUserBasic.flatMapLatestRich {
kaffeekasse.getExtendedUserInfo(it.id)
}
// private val selfUserExtended = selfUserBasic.flatMapLatestRich {
// kaffeekasse.getExtendedUserInfo(it.id)
// }
// override val purchaseAccounts: RichDataFlow<List<PurchaseAccount>> = combineRich(
// selfUserExtended,
......
......@@ -63,6 +63,7 @@ import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item
import net.novagamestudios.kaffeekasse.model.kaffeekasse.KnownItem
import net.novagamestudios.kaffeekasse.model.kaffeekasse.Transaction
import net.novagamestudios.kaffeekasse.model.session.Session
import net.novagamestudios.kaffeekasse.ui.theme.ifAnimationsEnabled
import net.novagamestudios.kaffeekasse.util.richdata.collectAsRichState
@Composable
......@@ -105,8 +106,8 @@ fun ItemCard(
AnimatedVisibility(
visible = count > 0,
Modifier.align(Alignment.CenterHorizontally),
enter = fadeIn(buttonsAnimationSpec()) + expandVertically(buttonsAnimationSpec()),
exit = fadeOut(buttonsAnimationSpec()) + shrinkVertically(buttonsAnimationSpec())
enter = (fadeIn(buttonsAnimationSpec()) + expandVertically(buttonsAnimationSpec())).ifAnimationsEnabled(),
exit = (fadeOut(buttonsAnimationSpec()) + shrinkVertically(buttonsAnimationSpec())).ifAnimationsEnabled()
) {
ItemAmountControls(
count = count,
......
package net.novagamestudios.kaffeekasse.ui.login
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.PhonelinkErase
import androidx.compose.material.icons.filled.ScreenLockLandscape
import androidx.compose.material.icons.filled.SmartScreen
import androidx.compose.material3.AlertDialog
......@@ -25,7 +17,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
......@@ -33,13 +24,11 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
......@@ -49,22 +38,20 @@ import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.launch
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.compose.components.CircularLoadingBox
import net.novagamestudios.kaffeekasse.R
import net.novagamestudios.kaffeekasse.app
import net.novagamestudios.kaffeekasse.data.cleanName
import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials
import net.novagamestudios.kaffeekasse.model.credentials.isValid
import net.novagamestudios.kaffeekasse.model.session.Device
import net.novagamestudios.kaffeekasse.model.session.Session
import net.novagamestudios.kaffeekasse.model.session.deviceOrNull
import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository
import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton
import net.novagamestudios.kaffeekasse.ui.login.UserSelectionScreenModel.Companion.backNavigationHandler
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler
import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere
......@@ -87,12 +74,6 @@ class LoginScreenModel private constructor(
}
}
fun logoutDevice() {
screenModelScope.launch {
loginRepository.logoutDevice()
}
}
companion object : ScreenModelFactory<LoginNavigation.Companion, LoginScreenModel> {
context (RepositoryProvider)
override fun create(screen: LoginNavigation.Companion) = LoginScreenModel(
......@@ -205,107 +186,46 @@ private fun LoginDeviceForm(
@Composable
fun LoginTopBarTitle(
model: LoginScreenModel
model: LoginScreenModel,
navigator: Navigator
) {
model.session.deviceOrNull?.let {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painterResource(R.drawable.logo_edited),
"Kaffeekasse",
Modifier.size(32.dp)
)
DeviceInfo(it, Modifier.padding(16.dp))
when (val screen = navigator.lastItem) {
is LoginNavigation.UserSelectionScreen -> {
UserSelectionTopBarTitle(screen.model, navigator)
}
}
}
context (RowScope)
@Composable
fun LoginTopBarActions(
model: LoginScreenModel
model: LoginScreenModel,
navigator: Navigator
) {
val session = model.session
if (session !is Session.WithRealUser) {
when (session) {
is Session.WithDevice -> {
var confirmLogout by remember { mutableStateOf(false) }
IconButton(
onClick = { confirmLogout = true },
enabled = !model.isLoading
) {
Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
}
if (confirmLogout) ConfirmLogoutDeviceDialog(
onDismiss = { confirmLogout = false },
onConfirm = { model.logoutDevice() }
)
}
else -> {
IconButton(
onClick = { model.showLoginDeviceDialog = true },
enabled = !model.isLoading
) {
Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
}
when (val screen = navigator.lastItem) {
is LoginNavigation.FormScreen -> {
AppInfoTopBarAction()
IconButton(
onClick = { model.showLoginDeviceDialog = true },
enabled = !model.isLoading
) {
Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen")
}
}
is LoginNavigation.UserSelectionScreen -> {
UserSelectionTopBarActions(screen.model, navigator)
}
}
AppInfoTopBarAction()
FullscreenIconButton()
}
@Composable
private fun DeviceInfo(
device: Device,
modifier: Modifier = Modifier
) = Row(
modifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(device.name ?: "Unbekanntes Gerät")
device.knownItemGroup?.let {
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
Spacer(Modifier.width(16.dp))
Icon(Icons.Default.ChevronRight, null)
Text(
it.cleanName,
style = MaterialTheme.typography.titleMedium
)
}
fun LoginScreenModel.backNavigationHandler(navigator: Navigator): BackNavigationHandler {
return when (val screen = navigator.lastItem) {
is LoginNavigation.UserSelectionScreen -> screen.model.backNavigationHandler(navigator)
else -> BackNavigationHandler.default()
}
}
@Composable
private fun ConfirmLogoutDeviceDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) = AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
Button(
onClick = onConfirm
) {
Text("Ausloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text("Abbrechen")
}
},
text = {
Text("Möchtest du das Gerät wirklich ausloggen?")
}
)
@Composable
fun LogoutTopBarAction(session: Session) {
......
package net.novagamestudios.kaffeekasse.ui.login
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
......@@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.grid.GridCells
......@@ -21,14 +23,13 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.PhonelinkErase
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
......@@ -36,7 +37,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
......@@ -56,6 +56,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
......@@ -76,11 +77,15 @@ import net.novagamestudios.common_utils.compose.components.RowCenter
import net.novagamestudios.common_utils.compose.state.rememberDerivedStateOf
import net.novagamestudios.common_utils.warn
import net.novagamestudios.kaffeekasse.App
import net.novagamestudios.kaffeekasse.R
import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.BasicUserInfo
import net.novagamestudios.kaffeekasse.data.cleanName
import net.novagamestudios.kaffeekasse.model.kaffeekasse.UserAuthCredentials
import net.novagamestudios.kaffeekasse.model.session.Device
import net.novagamestudios.kaffeekasse.repositories.LoginRepository
import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider
import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository
import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction
import net.novagamestudios.kaffeekasse.ui.handleSession
import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation
import net.novagamestudios.kaffeekasse.ui.util.AlphabetSelectionChar
......@@ -89,28 +94,34 @@ import net.novagamestudios.kaffeekasse.ui.util.HorizontalSelectionBar
import net.novagamestudios.kaffeekasse.ui.util.RichDataContent
import net.novagamestudios.kaffeekasse.ui.util.Toasts
import net.novagamestudios.kaffeekasse.ui.util.ToastsState
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchAction
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchField
import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchFieldState
import net.novagamestudios.kaffeekasse.ui.util.VerticalSelectionBar
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler
import net.novagamestudios.kaffeekasse.ui.util.rememberPullToRefreshState
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere
import net.novagamestudios.kaffeekasse.util.richdata.asRichDataFlow
import net.novagamestudios.kaffeekasse.util.richdata.combineRich
import net.novagamestudios.kaffeekasse.util.richdata.dataOrNull
import net.novagamestudios.kaffeekasse.util.richdata.stateIn
import net.novagamestudios.kaffeekasse.util.richdata.withFunctions
class UserSelectionScreenModel private constructor(
private val loginRepository: LoginRepository,
val device: Device,
val loginRepository: LoginRepository,
kaffeekasse: KaffeekasseRepository
) : ScreenModel, Logger {
val isPerformingLoginAction by loginRepository.isPerformingAction.collectAsStateHere()
val users = kaffeekasse.basicUserInfoList
var searchQuery by mutableStateOf("")
val searchState = TopBarSearchFieldState()
val filteredUsers = run {
val data = combineRich(
snapshotFlow { searchQuery.lowercase() }.asRichDataFlow(),
snapshotFlow { searchState.queryOrBlank.lowercase() }.asRichDataFlow(),
users,
loadDuringTransform = true
) { query, users ->
......@@ -160,12 +171,25 @@ class UserSelectionScreenModel private constructor(
loginUser(dialog.user, dialog.auth)
}
fun logoutDevice() {
screenModelScope.launch {
loginRepository.logoutDevice()
}
}
companion object : ScreenModelFactory<LoginNavigation.UserSelectionScreen, UserSelectionScreenModel> {
context (RepositoryProvider)
override fun create(screen: LoginNavigation.UserSelectionScreen) = UserSelectionScreenModel(
device = screen.device,
loginRepository = loginRepository,
kaffeekasse = kaffeekasseRepository,
)
@Composable
fun UserSelectionScreenModel.backNavigationHandler(navigator: Navigator): BackNavigationHandler {
return remember { searchState.backNavigation }
}
}
}
......@@ -186,19 +210,19 @@ fun UserSelection(
.nestedScroll(pullToRefreshState.nestedScrollConnection),
contentAlignment = Alignment.TopCenter
) {
UserSearchBar(
query = model.searchQuery,
onQueryChange = { model.searchQuery = it },
onSearch = {
model.filteredUsers.value
.dataOrNull
?.singleOrNull()
?.let { model.loginUser(it) }
},
Modifier
.padding(8.dp)
.align(Alignment.TopCenter)
)
// UserSearchBar(
// query = model.searchQuery,
// onQueryChange = { model.searchQuery = it },
// onSearch = {
// model.filteredUsers.value
// .dataOrNull
// ?.singleOrNull()
// ?.let { model.loginUser(it) }
// },
// Modifier
// .padding(8.dp)
// .align(Alignment.TopCenter)
// )
RichDataContent(
source = model.filteredUsers,
......@@ -240,6 +264,7 @@ fun UserSelection(
Toasts(model.toasts)
}
/*
@Composable
private fun UserSearchBar(
query: String,
......@@ -274,6 +299,7 @@ private fun UserSearchBar(
modifier,
content = { }
)
*/
@Composable
private fun UserGridWithSelectionBar(
......@@ -281,7 +307,8 @@ private fun UserGridWithSelectionBar(
onClick: (BasicUserInfo) -> Unit,
modifier: Modifier = Modifier
) {
val searchBarOffset = SearchBarDefaults.InputFieldHeight
// val searchBarOffset = SearchBarDefaults.InputFieldHeight
val searchBarOffset = 0.dp
val gridState = rememberLazyGridState()
val coroutineScope = rememberCoroutineScope()
......@@ -507,3 +534,94 @@ private fun UserAuthDialog(
}
}
)
@Composable
fun UserSelectionTopBarTitle(
model: UserSelectionScreenModel,
navigator: Navigator
) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (model.searchState.show) {
TopBarSearchField(model.searchState)
} else {
Image(
painterResource(R.drawable.logo_edited),
"Kaffeekasse",
Modifier.size(32.dp)
)
DeviceInfo(model.device, Modifier.padding(16.dp))
}
}
}
@Composable
fun UserSelectionTopBarActions(
model: UserSelectionScreenModel,
navigator: Navigator
) {
TopBarSearchAction(model.searchState, alwaysShow = true)
AppInfoTopBarAction()
var confirmLogout by remember { mutableStateOf(false) }
IconButton(
onClick = { confirmLogout = true },
enabled = !model.isPerformingLoginAction
) {
Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen")
}
if (confirmLogout) ConfirmLogoutDeviceDialog(
onDismiss = { confirmLogout = false },
onConfirm = { model.logoutDevice() }
)
}
@Composable
private fun DeviceInfo(
device: Device,
modifier: Modifier = Modifier
) = Row(
modifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(device.name ?: "Unbekanntes Gerät")
device.knownItemGroup?.let {
ProvideTextStyle(MaterialTheme.typography.titleMedium) {
Spacer(Modifier.width(16.dp))
Icon(Icons.Default.ChevronRight, null)
Text(
it.cleanName,
style = MaterialTheme.typography.titleMedium
)
}
}
}
@Composable
private fun ConfirmLogoutDeviceDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) = AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
Button(
onClick = onConfirm
) {
Text("Ausloggen")
}
},
modifier,
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text("Abbrechen")
}
},
text = {
Text("Möchtest du das Gerät wirklich ausloggen?")
}
)
......@@ -42,6 +42,7 @@ import net.novagamestudios.kaffeekasse.ui.login.LoginTopBarActions
import net.novagamestudios.kaffeekasse.ui.login.LoginTopBarTitle
import net.novagamestudios.kaffeekasse.ui.login.UserSelection
import net.novagamestudios.kaffeekasse.ui.login.UserSelectionScreenModel
import net.novagamestudios.kaffeekasse.ui.login.backNavigationHandler
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelProvider
import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenProvidingModel
......@@ -62,8 +63,8 @@ sealed interface LoginNavigation {
@Composable override fun Content() = AppScaffoldNavigator(
key = "login-navigator",
initialRoute = listOf(initialScreen),
title = { LoginTopBarTitle(model) },
actions = { LoginTopBarActions(model) },
title = { LoginTopBarTitle(model, it) },
actions = { LoginTopBarActions(model, it) },
topAppBarScrollBehavior = null,
content = {
if (model.isLoading) {
......@@ -74,7 +75,8 @@ sealed interface LoginNavigation {
AppScaffoldNavigatorDefaultContent("login-navigator-content")
}
LoginAdditional(model, this)
}
},
backNavigationHandlerProvider = { model.backNavigationHandler(it) }
)
}
......
package net.novagamestudios.kaffeekasse.ui.util
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler
class TopBarSearchFieldState {
private var _show by mutableStateOf(false)
var show
get() = _show
set(value) {
_show = value
if (!value) query = ""
}
var query by mutableStateOf("")
val queryOrBlank get() = query.takeIf { show } ?: ""
val queryOrNull get() = query.takeIf { show }?.takeUnless { it.isBlank() }
val backNavigation by lazy {
object : BackNavigationHandler {
override fun canNavigateBack() = show
override fun onNavigateBack(): Boolean {
if (show) {
show = false
return true
}
return false
}
}
}
}
@Composable
fun TopBarSearchField(
state: TopBarSearchFieldState,
modifier: Modifier = Modifier,
onSearch: () -> Unit = { }
) {
if (state.show) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
SearchBarDefaults.InputField(
query = state.query,
onQueryChange = { state.query = it },
onSearch = { onSearch() },
expanded = false,
onExpandedChange = { },
modifier = Modifier.focusRequester(focusRequester),
placeholder = { Text("Suchen") },
trailingIcon = {
if (state.query.isNotEmpty()) IconButton(onClick = { state.query = "" }) {
Icon(Icons.Default.Clear, "Clear")
}
}
)
}
}
@Composable
fun TopBarSearchAction(
state: TopBarSearchFieldState,
modifier: Modifier = Modifier,
alwaysShow: Boolean = false
) {
if (alwaysShow || !state.show) IconButton(
onClick = { state.show = !state.show },
modifier
) {
Icon(Icons.Default.Search, "Suchen")
}
}
......@@ -6,7 +6,10 @@ import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.launchIn
import net.novagamestudios.common_utils.Logger
import net.novagamestudios.common_utils.compose.components.Progress
import net.novagamestudios.common_utils.error
import kotlin.coroutines.suspendCoroutine
typealias RichDataCollector<T> = FlowCollector<RichData<T>>
......@@ -26,15 +29,19 @@ interface RichDataSource<out T : Any> : RichDataFunctions<T>, RichDataStateFlow<
private typealias Updater<T> = suspend RichDataCollector<T>.(RichData<T>) -> Unit
private class RichDataProducer<T : Any>(
coroutineScope: CoroutineScope,
private val coroutineScope: CoroutineScope,
producer: RichDataFactory<T>
) : RichDataSource<T> {
) : RichDataSource<T>, Logger {
private val wrappedProducer: Updater<T> = updater@{
emit(RichData.Loading(Progress.Indeterminate))
emit(producer())
}
private val updaterChannel = Channel<Updater<T>>(Channel.UNLIMITED)
private val updaterChannel = Channel<Updater<T>>(
Channel.UNLIMITED,
onUndeliveredElement = { error { "Failed to deliver updater" } }
)
private val outFlow: StateFlow<RichData<T>> = channelFlow {
var current: RichData<T> = RichData.None()
......@@ -62,9 +69,11 @@ private class RichDataProducer<T : Any>(
}
private fun enqueueUpdater(updater: Updater<T>) {
updaterChannel.trySend(updater)
if (!updaterChannel.trySend(updater).isSuccess) {
error { "Failed to enqueue updater" }
}
}
private suspend fun awaitUpdater(updater: Updater<T>): Unit = suspendCoroutine{ continuation ->
private suspend fun awaitUpdater(updater: Updater<T>): Unit = suspendCoroutine { continuation ->
enqueueUpdater { current ->
updater(current)
continuation.resumeWith(Result.success(Unit))
......@@ -80,14 +89,17 @@ private class RichDataProducer<T : Any>(
// }
}
override suspend fun ensureCleanData() = awaitUpdater { current ->
val needsUpdate = when (current) {
is RichData.Data -> current.isDirty
is RichData.Error -> true
is RichData.Loading -> false
is RichData.None -> true
override suspend fun ensureCleanData() {
outFlow.launchIn(coroutineScope) // Guarantee that the flow is started
awaitUpdater { current ->
val needsUpdate = when (current) {
is RichData.Data -> current.isDirty
is RichData.Error -> true
is RichData.Loading -> false
is RichData.None -> true
}
if (needsUpdate) wrappedProducer(current)
}
if (needsUpdate) wrappedProducer(current)
}
override suspend fun refresh() = awaitUpdater(wrappedProducer)
......
......@@ -92,7 +92,7 @@ dependencyResolutionManagement {
library("acra-limiter", "ch.acra", "acra-limiter").versionRef("acra")
library("acra-advancedscheduler", "ch.acra", "acra-advanced-scheduler").versionRef("acra")
library("commonutils", "com.gitlab.JojoIV", "common_utils").version("2d5e5c9a17")
library("commonutils", "com.gitlab.JojoIV", "common_utils").version("ee87e097c5")
}
}
}
......