35 Commits

Author SHA1 Message Date
661b88b961 Keep back stack of navigation controller clean.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-01-28 13:06:50 +01:00
c41816898e Add about page.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 12:33:45 +01:00
2e8d6a7a4e Add App name to navigation drawer.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 11:19:37 +01:00
ae0f85bec0 Rename screen to DrawerItem
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:25:07 +01:00
e1e25ff607 Move drawerContent to own file.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:14:06 +01:00
6aedb0d7f9 Navigate with enum defines. Create new TopBarState
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 10:06:58 +01:00
48374c5980 Move new game back to app bar.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-28 00:18:26 +01:00
02213f41b6 Add default android locale. Simplify language settings.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 23:09:08 +01:00
9ae0890f71 Restyle history page
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 20:52:17 +01:00
cd8f1959af Simplify history page. Fix warnings.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-27 20:13:14 +01:00
58d4fc0e43 Show blinking cursor in keyboard view. Give focus back to last focused input.
All checks were successful
continuous-integration/drone/push Build is passing
closes #12
2023-01-27 18:07:16 +01:00
c71b608a7b [#13] Hide keyboard in landscape.
All checks were successful
continuous-integration/drone/push Build is passing
closes #13
2023-01-27 13:18:00 +01:00
89f2e3ecd5 Improve default color scheme.
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-27 13:11:36 +01:00
f52bfa64ce [#18] Add a swap score button.
Some checks are pending
continuous-integration/drone/push Build is pending
closes #18
2023-01-27 12:37:59 +01:00
4346af3d2b Improve settings composable. Remove delay before setting the theme. as this did not help [#11]
Some checks are pending
continuous-integration/drone/push Build is pending
2023-01-27 12:03:36 +01:00
ca88bd1054 Move ui variable from viewModel to compose function. Move TopBar to separate file. 2023-01-27 10:22:32 +01:00
c54f63736e Enable drawer close gesture.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 21:42:38 +01:00
8d24e46687 Simplify database handling.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-22 21:40:47 +01:00
f40b66077b [#11] Add a small delay before applying theme so compose has enough time to update remember states before the theme is applied.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 20:45:45 +01:00
9ca830a707 [#11] ApplyDayNight should not be used.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 18:36:38 +01:00
db58e475d1 [#11] Change how settings adapter work. Directly set system settings in MainActivity.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-22 17:57:51 +01:00
33e57bcfd7 Show fab only on counter screen.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 18:21:40 +01:00
c1567efe52 [#11] Close dropdown list first. the callback might restart the application on a settings change and keep the menu open.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 18:12:30 +01:00
4d37e77f55 Fix round index.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 16:40:00 +01:00
984afb610f Add counter again to navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 16:32:07 +01:00
c3c6c253bc no message
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:58:18 +01:00
9189d79982 Disable gesture navigation.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:55:49 +01:00
ad0236556e Add delete all history with confirm dialog.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 15:48:07 +01:00
52a73bf204 Simplify history card. Activate open for active game. 2023-01-21 14:47:36 +01:00
6d0192df18 Style game history. Add functionality to delete and open game from history.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 13:04:45 +01:00
63f213bc75 Add hide show keyboard function. Fix keyboard too big on small screens. Improve colors.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 11:30:20 +01:00
68a5d34e45 Add versions code to VersionName.
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 10:21:51 +01:00
98ea66772f Fix warnings. Remove unused files.
Some checks are pending
continuous-integration/drone/push Build is running
2023-01-21 10:15:59 +01:00
84a63a2bcb Revert "Upload files to 'app'"
This reverts commit c5de57c416.
2023-01-21 09:02:20 +01:00
c5de57c416 Upload files to 'app'
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-21 07:48:21 +01:00
64 changed files with 1473 additions and 860 deletions

View File

@@ -15,6 +15,8 @@ def versionPropertiesFile = rootProject.file("version.properties")
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def versionProperties = new Properties() def versionProperties = new Properties()
def versionMajor = 2
def versionMinor = 0
// Load your keystore.properties file into the keystoreProperties object. // Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
@@ -29,13 +31,19 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode versionProperties["versionCode"].toInteger() versionCode versionProperties["versionCode"].toInteger()
versionName "1.1.0Beta1" versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resConfigs 'de', 'en' resConfigs 'de', 'en'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
} }
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
signingConfigs { signingConfigs {
create("release") { create("release") {
@@ -86,7 +94,7 @@ dependencies {
implementation "androidx.compose.material3:material3:1.0.1" implementation "androidx.compose.material3:material3:1.0.1"
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1'
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'

View File

@@ -0,0 +1,102 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "1739540cd7d5436941316932a1036d83",
"entities": [
{
"tableName": "Round",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gameId` INTEGER NOT NULL, `scoreA` INTEGER NOT NULL, `scoreB` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "gameId",
"columnName": "gameId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreA",
"columnName": "scoreA",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreB",
"columnName": "scoreB",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`active` INTEGER NOT NULL, `nameA` TEXT NOT NULL, `nameB` TEXT NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "nameA",
"columnName": "nameA",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nameB",
"columnName": "nameB",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modified",
"columnName": "modified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1739540cd7d5436941316932a1036d83')"
]
}
}

View File

@@ -0,0 +1,102 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "1739540cd7d5436941316932a1036d83",
"entities": [
{
"tableName": "Round",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`gameId` INTEGER NOT NULL, `scoreA` INTEGER NOT NULL, `scoreB` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "gameId",
"columnName": "gameId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreA",
"columnName": "scoreA",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scoreB",
"columnName": "scoreB",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Game",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`active` INTEGER NOT NULL, `nameA` TEXT NOT NULL, `nameB` TEXT NOT NULL, `created` INTEGER NOT NULL, `modified` INTEGER NOT NULL, `uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
"fields": [
{
"fieldPath": "active",
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "nameA",
"columnName": "nameA",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nameB",
"columnName": "nameB",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "modified",
"columnName": "modified",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1739540cd7d5436941316932a1036d83')"
]
}
}

View File

@@ -1,83 +0,0 @@
package me.zobrist.tichucounter
import android.content.SharedPreferences
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Theme
import javax.inject.Inject
@AndroidEntryPoint
abstract class BaseActivity : AppCompatActivity(),
SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
lateinit var settingsAdapter: SettingsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
keepScreenOn(settingsAdapter.keepScreenOn)
updateTheme(settingsAdapter.theme)
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onResume() {
super.onResume()
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) {
settingsAdapter.language::class.simpleName -> setLanguage(settingsAdapter.language)
settingsAdapter.keepScreenOn::class.simpleName -> keepScreenOn(settingsAdapter.keepScreenOn)
settingsAdapter.theme::class.simpleName -> updateTheme(settingsAdapter.theme)
}
}
private fun updateTheme(theme: Theme) {
val themeValue = when (theme) {
Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
Theme.DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
if (themeValue != AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.setDefaultNightMode(themeValue)
delegate.applyDayNight()
}
}
private fun keepScreenOn(keepOn: Boolean) {
if (keepOn) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
private fun setLanguage(language: Language) {
val currentLocale = AppCompatDelegate.getApplicationLocales()[0].toString()
if (language.value != null && language.value != currentLocale) {
val newLocale = LocaleListCompat.forLanguageTags(language.value)
AppCompatDelegate.setApplicationLocales(newLocale)
}
}
}

View File

@@ -1,9 +1,11 @@
package me.zobrist.tichucounter package me.zobrist.tichucounter
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -11,38 +13,34 @@ import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.NavigationAction import me.zobrist.tichucounter.domain.*
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.repository.GameRepository
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.MainViewModel import me.zobrist.tichucounter.ui.MainViewModel
import me.zobrist.tichucounter.ui.about.AboutView
import me.zobrist.tichucounter.ui.composables.DropDownMenu
import me.zobrist.tichucounter.ui.counter.* import me.zobrist.tichucounter.ui.counter.*
import me.zobrist.tichucounter.ui.history.HistoryList import me.zobrist.tichucounter.ui.history.HistoryList
import me.zobrist.tichucounter.ui.history.HistoryViewModel import me.zobrist.tichucounter.ui.history.HistoryViewModel
import me.zobrist.tichucounter.ui.layout.DrawerContent
import me.zobrist.tichucounter.ui.layout.TopBar
import me.zobrist.tichucounter.ui.settings.SettingsView import me.zobrist.tichucounter.ui.settings.SettingsView
import me.zobrist.tichucounter.ui.settings.SettingsViewModel import me.zobrist.tichucounter.ui.settings.SettingsViewModel
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : BaseActivity() { class MainActivity : AppCompatActivity(), ISettingsChangeListener {
@Inject @Inject
lateinit var gameRepository: GameRepository lateinit var settingsAdapter: SettingsAdapter
private val counterViewModel: CounterViewModel by viewModels() private val counterViewModel: CounterViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels()
@@ -53,8 +51,10 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
settingsAdapter.registerOnChangeListener(this)
setContent { setContent {
AppTheme() { AppTheme {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background) systemUiController.setStatusBarColor(MaterialTheme.colorScheme.background)
NavigationDrawer() NavigationDrawer()
@@ -62,102 +62,30 @@ class MainActivity : BaseActivity() {
} }
} }
@OptIn(ExperimentalMaterial3Api::class) override fun onDestroy() {
@Composable super.onDestroy()
fun MyScaffoldLayout( settingsAdapter.unregisterOnChangeListener(this)
drawerState: DrawerState,
scope: CoroutineScope,
navController: NavHostController
) {
Scaffold(
topBar = {
TopBar(
mainViewModel.topBarTitle,
mainViewModel.topBarIcon,
{ mainViewModel.onNavigateClicked() },
mainViewModel.topBarActions
)
}) {
NavHost(
navController = navController,
startDestination = "counter",
modifier = Modifier.padding(it)
) {
composable("counter") {
Counter(counterViewModel)
mainViewModel.topBarActions = (listOf(
TopBarAction(
Icons.Outlined.Undo,
mainViewModel.isUndoActionActive
) { mainViewModel.undoLastRound() },
TopBarAction(
Icons.Outlined.Redo,
mainViewModel.isRedoActionActive
) { mainViewModel.redoLastRound() }
))
mainViewModel.topBarIcon = Icons.Outlined.Menu
mainViewModel.topBarTitle = stringResource(R.string.app_name)
mainViewModel.topBarNavigationAction =
NavigationAction { scope.launch { drawerState.open() } }
}
composable("history") {
HistoryList(historyViewModel)
mainViewModel.topBarActions = emptyList()
mainViewModel.topBarIcon = Icons.Outlined.ArrowBack
mainViewModel.topBarTitle = stringResource(R.string.menu_history)
mainViewModel.topBarNavigationAction =
NavigationAction { navController.navigate("counter") }
}
composable("settings") {
SettingsView(settingsViewModel)
mainViewModel.topBarActions = emptyList()
mainViewModel.topBarIcon = Icons.Outlined.ArrowBack
mainViewModel.topBarTitle = stringResource(R.string.menu_settings)
mainViewModel.topBarNavigationAction =
NavigationAction { navController.navigate("counter") }
}
}
}
} }
@OptIn(ExperimentalMaterial3Api::class) override fun onLanguageChanged(language: Language) {
@Composable AppCompatDelegate.setApplicationLocales(language.value)
private fun TopBar(
title: String,
icon: ImageVector,
navigateAction: () -> Unit,
actions: List<TopBarAction>
) {
TopAppBar(
title = {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navigateAction() }) {
Icon(
imageVector = icon,
contentDescription = "Localized description"
)
} }
},
actions = { override fun onThemeChanged(theme: Theme) {
actions.forEach { val themeValue = when (theme) {
IconButton(onClick = { it.action() }, enabled = it.isActive) { Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Icon( Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
imageVector = it.imageVector, Theme.DEFAULT -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
contentDescription = null,
)
} }
AppCompatDelegate.setDefaultNightMode(themeValue)
} }
override fun onScreenOnChanged(keepOn: KeepScreenOn) {
if (keepOn.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} }
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -168,79 +96,139 @@ class MainActivity : BaseActivity() {
val navController = rememberNavController() val navController = rememberNavController()
val items = listOf( val items = listOf(
Screen("history", Icons.Outlined.List, R.string.menu_history),Screen("settings", Icons.Outlined.Settings, R.string.menu_settings) DrawerItem(
Route.COUNTER,
Icons.Outlined.Calculate,
stringResource(R.string.menu_counter)
),
DrawerItem(Route.HISTORY, Icons.Outlined.List, stringResource(R.string.menu_history)),
DrawerItem(
Route.SETTINGS,
Icons.Outlined.Settings,
stringResource(R.string.menu_settings)
),
DrawerItem(
Route.ABOUT,
Icons.Outlined.Info,
stringResource(R.string.menu_about)
)
) )
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination val currentDestination =
Route.valueOf(navBackStackEntry?.destination?.route ?: Route.COUNTER.name)
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
gesturesEnabled = drawerState.isOpen,
drawerContent = { drawerContent = {
ModalDrawerSheet { DrawerContent(
drawerItems = items,
selectedDrawerItem = items.first { it.route == currentDestination }) {
scope.launch {
drawerState.close()
Spacer(Modifier.height(20.dp))
NavigationDrawerItem(
icon = { Icon(Icons.Outlined.RestartAlt, contentDescription = null) },
colors = NavigationDrawerItemDefaults.colors(
unselectedContainerColor = MaterialTheme.colorScheme.secondaryContainer
),
label = { Text(stringResource(R.string.newGame)) },
selected = false,
onClick = {
scope.launch { drawerState.close() }
mainViewModel.newGame()
navController.navigate("counter") {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
} }
// Avoid multiple copies of the same destination when navController.navigate(it)
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
} }
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
Spacer(Modifier.height(20.dp))
Divider()
items.forEach { screen ->
NavigationDrawerItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
scope.launch { drawerState.close() }
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
} }
// Avoid multiple copies of the same destination when ) {
// reselecting the same item MyScaffoldLayout(
launchSingleTop = true drawerState,
// Restore state when reselecting a previously selected item scope,
restoreState = true navController,
counterViewModel.keyboardHidden && (currentDestination == Route.COUNTER)
) { counterViewModel.showKeyboard() }
} }
}, }
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
) @OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyScaffoldLayout(
drawerState: DrawerState,
scope: CoroutineScope,
navController: NavHostController,
showFab: Boolean,
fabAction: () -> Unit
) {
var topBarState by remember { mutableStateOf(TopBarState()) }
Scaffold(
floatingActionButton = {
if (showFab) {
FloatingActionButton(
onClick = { fabAction() }) {
Icon(Icons.Outlined.Keyboard, null)
} }
} }
}, },
content = { MyScaffoldLayout(drawerState, scope, navController) } topBar = { TopBar(topBarState) }) { paddings ->
)
NavHost(
navController = navController,
startDestination = Route.COUNTER.name,
modifier = Modifier.padding(paddings)
) {
composable(Route.COUNTER) {
var expanded by remember { mutableStateOf(false) }
topBarState = TopBarState(
title = stringResource(R.string.app_name),
actions = (listOf(
TopBarAction(
Icons.Outlined.Undo,
mainViewModel.isUndoActionActive,
{ mainViewModel.undoLastRound() }),
TopBarAction(
Icons.Outlined.Redo,
mainViewModel.isRedoActionActive,
{ mainViewModel.redoLastRound() }),
TopBarAction(
Icons.Outlined.MoreVert,
mainViewModel.activeGameHasRounds,
{ expanded = true }
) {
DropDownMenu(
mapOf("new" to R.string.newGame),
"",
expanded,
) {
expanded = false
it?.let {
when (it) {
"new" -> mainViewModel.newGame()
}
}
}
},
))
) { scope.launch { drawerState.open() } }
Counter(counterViewModel)
}
composable(Route.HISTORY) {
topBarState =
TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } }
HistoryList(historyViewModel) { navController.navigate(Route.COUNTER) }
}
composable(Route.SETTINGS) {
topBarState =
TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } }
SettingsView(settingsViewModel)
} }
private class Screen(val route: String, val icon: ImageVector, @StringRes val resourceId: Int) composable(Route.ABOUT) {
topBarState =
TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } }
AboutView()
}
}
}
}
} }

View File

@@ -3,6 +3,8 @@ package me.zobrist.tichucounter.data
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
@Database(entities = [Round::class, Game::class], version = 1) @Database(entities = [Round::class, Game::class], version = 1)
@TypeConverters(DateConverter::class) @TypeConverters(DateConverter::class)

View File

@@ -16,4 +16,7 @@ interface DaoBase<T> {
@Delete @Delete
fun delete(entity: T) fun delete(entity: T)
@Delete
fun delete(entity: List<T>)
} }

View File

@@ -1,15 +0,0 @@
package me.zobrist.tichucounter.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
@Entity
data class Game(
override var active: Boolean,
override var nameA: String,
override var nameB: String,
override val created: Date,
override var modified: Date,
@PrimaryKey(autoGenerate = true) override val uid: Long? = null
) : IGame, IEntity

View File

@@ -1,17 +0,0 @@
package me.zobrist.tichucounter.data
import androidx.room.Entity
import java.util.*
@Entity
data class GameAndScore(
override var active: Boolean,
override var nameA: String,
override var nameB: String,
override val created: Date,
override var modified: Date,
override var gameId: Long,
override var scoreA: Int,
override var scoreB: Int,
) : IGame, IRound {
}

View File

@@ -2,6 +2,7 @@ package me.zobrist.tichucounter.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import me.zobrist.tichucounter.data.entity.Game
@Dao @Dao
@@ -10,19 +11,13 @@ interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game") @Query("SELECT * FROM game")
fun getAll(): Flow<List<Game>> fun getAll(): Flow<List<Game>>
@Query( @Transaction
"SELECT active, " + @Query("SELECT * FROM game where uid ")
"nameA, " + fun getGamesWithRounds(): Flow<List<GameWithScores>>
"nameB, " +
"created, " + @Transaction
"modified, " + @Query("SELECT * FROM game WHERE active is 1")
"game.uid as gameId, " + fun getActiveWithRounds(): Flow<GameWithScores?>
"COALESCE(SUM(round.scoreA), 0) as scoreA, " +
"COALESCE(SUM(round.scoreB), 0) as scoreB " +
"FROM game " +
"LEFT JOIN round ON round.gameId = game.uid GROUP BY game.uid ORDER BY modified DESC"
)
fun getAllWithPoints(): Flow<List<GameAndScore>>
@Query("SELECT * FROM game WHERE uid is :gameId") @Query("SELECT * FROM game WHERE uid is :gameId")
fun getGameById(gameId: Long): Flow<Game> fun getGameById(gameId: Long): Flow<Game>
@@ -30,6 +25,7 @@ interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game WHERE active is 1") @Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Flow<Game?> fun getActive(): Flow<Game?>
@Query("UPDATE game SET active = 1 WHERE uid is :gameId;") @Query("UPDATE game SET active = 1 WHERE uid is :gameId;")
fun setActive(gameId: Long) fun setActive(gameId: Long)

View File

@@ -0,0 +1,17 @@
package me.zobrist.tichucounter.data
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Relation
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
@Entity
data class GameWithScores(
@Embedded val game: Game,
@Relation(
parentColumn = "uid",
entityColumn = "gameId"
)
val rounds: List<Round>
)

View File

@@ -1,5 +0,0 @@
package me.zobrist.tichucounter.data
interface IEntity {
val uid: Long?
}

View File

@@ -1,11 +0,0 @@
package me.zobrist.tichucounter.data
import java.util.*
interface IGame {
var active: Boolean
var nameA: String
var nameB: String
val created: Date
var modified: Date
}

View File

@@ -1,7 +0,0 @@
package me.zobrist.tichucounter.data
interface IRound {
var gameId: Long
var scoreA: Int
var scoreB: Int
}

View File

@@ -1,12 +0,0 @@
package me.zobrist.tichucounter.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Round(
override var gameId: Long,
override var scoreA: Int,
override var scoreB: Int,
@PrimaryKey(autoGenerate = true) override val uid: Long? = null
) : IRound, IEntity

View File

@@ -1,27 +1,15 @@
package me.zobrist.tichucounter.data package me.zobrist.tichucounter.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow import me.zobrist.tichucounter.data.entity.Round
@Dao @Dao
interface RoundDao : DaoBase<Round> { interface RoundDao : DaoBase<Round> {
@Query("SELECT * FROM round")
fun getAll(): List<Round>
@Query("SELECT * FROM round WHERE gameId is :gameId") @Query("SELECT * FROM round WHERE gameId is :gameId")
fun getAllForGame(gameId: Long?): List<Round> fun getAllForGame(gameId: Long?): List<Round>
@Query(
"SELECT gameId, SUM(scoreA) as scoreA, SUM(scoreB) as scoreB " +
"FROM round " +
"LEFT JOIN game ON game.uid = round.gameId " +
"WHERE game.active == 1"
)
fun getRoundSumForActiveGame(): Flow<Round>
@Query(
"SELECT gameId, scoreA, scoreB, round.uid " +
"FROM round " +
"LEFT JOIN game ON game.uid = round.gameId " +
"WHERE game.active == 1"
)
fun getForActiveGame(): Flow<List<Round>>
} }

View File

@@ -0,0 +1,15 @@
package me.zobrist.tichucounter.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
@Entity
data class Game(
var active: Boolean,
var nameA: String,
var nameB: String,
val created: Date,
var modified: Date,
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity

View File

@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.data.entity
interface IEntity {
val uid: Long
}

View File

@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Round(
var gameId: Long,
var scoreA: Int,
var scoreB: Int,
@PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity

View File

@@ -0,0 +1,5 @@
package me.zobrist.tichucounter.domain
import androidx.compose.ui.graphics.vector.ImageVector
data class DrawerItem(val route: Route, val menuIcon: ImageVector, val menuName: String)

View File

@@ -0,0 +1,14 @@
package me.zobrist.tichucounter.domain
import me.zobrist.tichucounter.data.GameWithScores
fun GameWithScores.getTotalPoints(): Pair<Int, Int> {
var scoreA = 0
var scoreB = 0
this.rounds.forEach {
scoreA += it.scoreA
scoreB += it.scoreB
}
return Pair(scoreA, scoreB)
}

View File

@@ -1,7 +1,5 @@
package me.zobrist.tichucounter.domain package me.zobrist.tichucounter.domain
import kotlin.math.abs
fun Int.isMultipleOf5(): Boolean { fun Int.isMultipleOf5(): Boolean {
return (this % 5) == 0 return (this % 5) == 0
@@ -11,6 +9,3 @@ fun Int.isMultipleOf100(): Boolean {
return (this % 100) == 0 return (this % 100) == 0
} }
fun Int.getAbsoluteDifference(other: Int): Int {
return abs(this - other)
}

View File

@@ -0,0 +1,30 @@
package me.zobrist.tichucounter.domain
import androidx.compose.runtime.Composable
import androidx.navigation.*
import androidx.navigation.compose.composable
fun NavController.navigate(route: Route) {
this.navigate(route.name){
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(Route.COUNTER.name) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
fun NavGraphBuilder.composable(
route: Route,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
this.composable(route.name, arguments, deepLinks, content)
}

View File

@@ -1,3 +0,0 @@
package me.zobrist.tichucounter.domain
class NavigationAction(val aciton: () -> Unit)

View File

@@ -0,0 +1,3 @@
package me.zobrist.tichucounter.domain
enum class Route { COUNTER, HISTORY, SETTINGS, ABOUT }

View File

@@ -1,65 +1,116 @@
package me.zobrist.tichucounter.domain package me.zobrist.tichucounter.domain
import android.content.Context import android.content.Context
import androidx.appcompat.app.AppCompatDelegate.getApplicationLocales import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
enum class Theme { DEFAULT, DARK, LIGHT } enum class Theme { DEFAULT, DARK, LIGHT }
enum class Language(val value: String) { ENGLISH("en"), GERMAN("de") } enum class Language(val value: LocaleListCompat) {
DEFAULT(LocaleListCompat.getEmptyLocaleList()),
ENGLISH(LocaleListCompat.forLanguageTags("en")),
GERMAN(LocaleListCompat.forLanguageTags("de"))
}
enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) }
interface ISettingsChangeListener {
fun onLanguageChanged(language: Language)
fun onThemeChanged(theme: Theme)
fun onScreenOnChanged(keepOn: KeepScreenOn)
}
@Singleton
class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) { class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private var listenerList = mutableListOf<ISettingsChangeListener>()
val language: Language var language: Language
get() { private set
var setting = sharedPreferences.getString(Language::class.simpleName, null)
if (setting == null) { var theme: Theme
setCurrentLanguage() private set
setting =
sharedPreferences.getString(Language::class.simpleName, Language.ENGLISH.name) var keepScreenOn: KeepScreenOn
} private set
return enumValueOf(setting!!)
init {
language = try {
enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!)
} catch (_: NullPointerException) {
Language.DEFAULT
} }
val theme: Theme theme = try {
get() { enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!)
val setting = sharedPreferences.getString(Theme::class.simpleName, Theme.DEFAULT.name) } catch (_: java.lang.Exception) {
return enumValueOf(setting!!) Theme.DEFAULT
} }
val keepScreenOn: Boolean keepScreenOn = try {
get() { enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!)
return sharedPreferences.getBoolean("keep_screen_on", false) } catch (_: java.lang.Exception) {
KeepScreenOn.OFF
}
} }
private fun setCurrentLanguage() { fun registerOnChangeListener(listener: ISettingsChangeListener) {
var setting = when (getApplicationLocales()[0].toString()) { listenerList.add(listener)
"de" -> Language.GERMAN
else -> Language.ENGLISH listener.onThemeChanged(theme)
listener.onLanguageChanged(language)
listener.onScreenOnChanged(keepScreenOn)
}
fun unregisterOnChangeListener(listener: ISettingsChangeListener?) {
if (listener != null) {
listenerList.remove(listener)
} }
setLanguage(setting)
} }
fun setLanguage(language: Language) { fun setLanguage(language: Language) {
val editor = sharedPreferences.edit() this.language = language
editor.putString(Language::class.simpleName, language.name) updatePreference(Language::class.simpleName, language.name)
editor.commit() notifyListeners(language)
} }
fun setTheme(theme: Theme) { fun setTheme(theme: Theme) {
val editor = sharedPreferences.edit() this.theme = theme
editor.putString(Theme::class.simpleName, theme.name) updatePreference(Theme::class.simpleName, theme.name)
editor.commit() notifyListeners(theme)
} }
fun setKeepScreenOn(setting: Boolean) { fun setKeepScreenOn(setting: KeepScreenOn) {
this.keepScreenOn = setting
updatePreference(KeepScreenOn::class.simpleName, setting.name)
notifyListeners(setting)
}
private fun updatePreference(name: String?, value: String) {
val editor = sharedPreferences.edit() val editor = sharedPreferences.edit()
editor.putBoolean("keep_screen_on", setting) editor.putString(name, value)
editor.commit() editor.apply()
}
private fun notifyListeners(language: Language) {
listenerList.forEach {
it.onLanguageChanged(language)
}
}
private fun notifyListeners(theme: Theme) {
listenerList.forEach {
it.onThemeChanged(theme)
}
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
listenerList.forEach {
it.onScreenOnChanged(keepScreenOn)
}
} }
} }

View File

@@ -5,9 +5,6 @@ import javax.inject.Inject
class Tichu @Inject constructor() { class Tichu @Inject constructor() {
fun calculateOtherScore(score: Int): Int? { fun calculateOtherScore(score: Int): Int? {
if (score == null) {
return null
}
if (!score.isMultipleOf5()) { if (!score.isMultipleOf5()) {
return null return null
} }
@@ -24,6 +21,6 @@ class Tichu @Inject constructor() {
if (scoreA == null || scoreB == null) { if (scoreA == null || scoreB == null) {
return false return false
} }
return (scoreA!!.isMultipleOf5()) && scoreB!!.isMultipleOf5() && (scoreA!! + scoreB!!).isMultipleOf100() return (scoreA.isMultipleOf5()) && scoreB.isMultipleOf5() && (scoreA + scoreB).isMultipleOf100()
} }
} }

View File

@@ -1,5 +1,11 @@
package me.zobrist.tichucounter.domain package me.zobrist.tichucounter.domain
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
class TopBarAction(val imageVector: ImageVector, val isActive: Boolean, val action: () -> Unit) class TopBarAction(
val imageVector: ImageVector,
val isActive: Boolean,
val action: () -> Unit,
val composeCode: @Composable () -> Unit = {}
)

View File

@@ -0,0 +1,12 @@
package me.zobrist.tichucounter.domain
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.ui.graphics.vector.ImageVector
data class TopBarState(
var title: String = "",
var icon: ImageVector = Icons.Outlined.Menu,
var actions: List<TopBarAction> = emptyList(),
var onNavigate: () -> Unit = {}
)

View File

@@ -1,55 +0,0 @@
package me.zobrist.tichucounter.framework
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private const val TAG = "me.zobrist.tichucounter.framework.SingleLiveEvent"
}
}

View File

@@ -2,12 +2,15 @@ package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.Game
import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@@ -50,7 +53,7 @@ class GameRepository @Inject constructor(
} }
} }
private suspend fun setActive(id: Long) { suspend fun setActive(id: Long) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
gameDao.setActive(id) gameDao.setActive(id)
gameDao.setOthersInactive(id) gameDao.setOthersInactive(id)
@@ -80,9 +83,47 @@ class GameRepository @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val active = activeGame val active = activeGame
active.modified = Date() active.modified = Date()
val round = Round(active.uid!!, scoreA, scoreB) val round = Round(active.uid, scoreA, scoreB)
roundDao.insert(round) roundDao.insert(round)
gameDao.update(active) gameDao.update(active)
} }
} }
suspend fun deleteGame(uid: Long) {
withContext(Dispatchers.IO) {
try {
gameDao.getGameById(uid).take(1).collect {
gameDao.delete(it)
val rounds = roundDao.getAllForGame(it.uid)
roundDao.delete(rounds)
}
} catch (_: NullPointerException) {
}
}
}
suspend fun deleteAllInactive() {
withContext(Dispatchers.IO) {
try {
gameDao.getAll().take(1).collect { games ->
val activeId = games.first { it.active }.uid
val gamesToDelete = games.filter { !it.active }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeId }
gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete)
}
} catch (_: NullPointerException) {
}
}
}
fun getActiveGameFlow(): Flow<GameWithScores?> {
return gameDao.getActiveWithRounds()
}
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
} }

View File

@@ -1,19 +1,21 @@
@file:Suppress("unused")
package me.zobrist.tichucounter.ui package me.zobrist.tichucounter.ui
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFFBE0034) val md_theme_light_primary = Color(0xFF9C404D)
val md_theme_light_onPrimary = Color(0xFFFFFFFF) val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDADA) val md_theme_light_primaryContainer = Color(0xFFFFDADB)
val md_theme_light_onPrimaryContainer = Color(0xFF40000B) val md_theme_light_onPrimaryContainer = Color(0xFF40000F)
val md_theme_light_secondary = Color(0xFF6E5D00) val md_theme_light_secondary = Color(0xFF765659)
val md_theme_light_onSecondary = Color(0xFFFFFFFF) val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFFE261) val md_theme_light_secondaryContainer = Color(0xFFFFDADB)
val md_theme_light_onSecondaryContainer = Color(0xFF221B00) val md_theme_light_onSecondaryContainer = Color(0xFF2C1517)
val md_theme_light_tertiary = Color(0xFF76592F) val md_theme_light_tertiary = Color(0xFF775930)
val md_theme_light_onTertiary = Color(0xFFFFFFFF) val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFDDB1) val md_theme_light_tertiaryContainer = Color(0xFFFFDDB5)
val md_theme_light_onTertiaryContainer = Color(0xFF291800) val md_theme_light_onTertiaryContainer = Color(0xFF2A1800)
val md_theme_light_error = Color(0xFFBA1A1A) val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6) val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF) val md_theme_light_onError = Color(0xFFFFFFFF)
@@ -22,29 +24,29 @@ val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF201A1A) val md_theme_light_onBackground = Color(0xFF201A1A)
val md_theme_light_surface = Color(0xFFFFFBFF) val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF201A1A) val md_theme_light_onSurface = Color(0xFF201A1A)
val md_theme_light_surfaceVariant = Color(0xFFF4DDDD) val md_theme_light_surfaceVariant = Color(0xFFF4DDDE)
val md_theme_light_onSurfaceVariant = Color(0xFF524343) val md_theme_light_onSurfaceVariant = Color(0xFF524344)
val md_theme_light_outline = Color(0xFF857373) val md_theme_light_outline = Color(0xFF857374)
val md_theme_light_inverseOnSurface = Color(0xFFFBEEED) val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
val md_theme_light_inverseSurface = Color(0xFF362F2F) val md_theme_light_inverseSurface = Color(0xFF362F2F)
val md_theme_light_inversePrimary = Color(0xFFFFB3B5) val md_theme_light_inversePrimary = Color(0xFFFFB2B9)
val md_theme_light_shadow = Color(0xFF000000) val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFFBE0034) val md_theme_light_surfaceTint = Color(0xFF9C404D)
val md_theme_light_outlineVariant = Color(0xFFD7C1C1) val md_theme_light_outlineVariant = Color(0xFFD7C1C2)
val md_theme_light_scrim = Color(0xFF000000) val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB3B5) val md_theme_dark_primary = Color(0xFFFFB2B9)
val md_theme_dark_onPrimary = Color(0xFF680018) val md_theme_dark_onPrimary = Color(0xFF5F1222)
val md_theme_dark_primaryContainer = Color(0xFF920026) val md_theme_dark_primaryContainer = Color(0xFF7D2937)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADA) val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADB)
val md_theme_dark_secondary = Color(0xFFE6C500) val md_theme_dark_secondary = Color(0xFFE5BDBF)
val md_theme_dark_onSecondary = Color(0xFF3A3000) val md_theme_dark_onSecondary = Color(0xFF44292C)
val md_theme_dark_secondaryContainer = Color(0xFF534600) val md_theme_dark_secondaryContainer = Color(0xFF5C3F41)
val md_theme_dark_onSecondaryContainer = Color(0xFFFFE261) val md_theme_dark_onSecondaryContainer = Color(0xFFFFDADB)
val md_theme_dark_tertiary = Color(0xFFE6C18D) val md_theme_dark_tertiary = Color(0xFFE8C08E)
val md_theme_dark_onTertiary = Color(0xFF422C05) val md_theme_dark_onTertiary = Color(0xFF442B06)
val md_theme_dark_tertiaryContainer = Color(0xFF5C421A) val md_theme_dark_tertiaryContainer = Color(0xFF5D411B)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB1) val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB5)
val md_theme_dark_error = Color(0xFFFFB4AB) val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A) val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005) val md_theme_dark_onError = Color(0xFF690005)
@@ -53,16 +55,16 @@ val md_theme_dark_background = Color(0xFF201A1A)
val md_theme_dark_onBackground = Color(0xFFECE0DF) val md_theme_dark_onBackground = Color(0xFFECE0DF)
val md_theme_dark_surface = Color(0xFF201A1A) val md_theme_dark_surface = Color(0xFF201A1A)
val md_theme_dark_onSurface = Color(0xFFECE0DF) val md_theme_dark_onSurface = Color(0xFFECE0DF)
val md_theme_dark_surfaceVariant = Color(0xFF524343) val md_theme_dark_surfaceVariant = Color(0xFF524344)
val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C1) val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C2)
val md_theme_dark_outline = Color(0xFF9F8C8C) val md_theme_dark_outline = Color(0xFF9F8C8D)
val md_theme_dark_inverseOnSurface = Color(0xFF201A1A) val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
val md_theme_dark_inverseSurface = Color(0xFFECE0DF) val md_theme_dark_inverseSurface = Color(0xFFECE0DF)
val md_theme_dark_inversePrimary = Color(0xFFBE0034) val md_theme_dark_inversePrimary = Color(0xFF9C404D)
val md_theme_dark_shadow = Color(0xFF000000) val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB3B5) val md_theme_dark_surfaceTint = Color(0xFFFFB2B9)
val md_theme_dark_outlineVariant = Color(0xFF524343) val md_theme_dark_outlineVariant = Color(0xFF524344)
val md_theme_dark_scrim = Color(0xFF000000) val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFFED0043) val seed = Color(0xFF833842)

View File

@@ -1,7 +1,5 @@
package me.zobrist.tichucounter.ui package me.zobrist.tichucounter.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -10,48 +8,45 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.domain.NavigationAction
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val gameRepository: GameRepository, private val gameRepository: GameRepository
roundDao: RoundDao
) : ViewModel() { ) : ViewModel() {
private var redoRounds = mutableStateListOf<Round>() private var redoRounds = mutableStateListOf<Round>()
private var expectedRoundCount = 0 private var expectedRoundCount = 0
var topBarTitle by mutableStateOf("")
var topBarActions by mutableStateOf(emptyList<TopBarAction>())
var topBarIcon by mutableStateOf(Icons.Filled.Menu)
var isUndoActionActive by mutableStateOf(false) var isUndoActionActive by mutableStateOf(false)
var topBarNavigationAction by mutableStateOf(NavigationAction {})
val isRedoActionActive: Boolean val isRedoActionActive: Boolean
get() = redoRounds.isNotEmpty() get() = redoRounds.isNotEmpty()
var activeGameHasRounds by mutableStateOf(false)
private set
init { init {
viewModelScope.launch { viewModelScope.launch {
roundDao.getForActiveGame().collect() {
isUndoActionActive = it.isNotEmpty()
if (expectedRoundCount != it.count()) { gameRepository.getActiveGameFlow().collect {
activeGameHasRounds = it?.rounds?.isNotEmpty() == true
if (it != null) {
isUndoActionActive = it.rounds.isNotEmpty()
if (expectedRoundCount != it.rounds.count()) {
redoRounds.clear() redoRounds.clear()
} }
expectedRoundCount = it.count() expectedRoundCount = it.rounds.count()
} }
} }
} }
fun onNavigateClicked() {
topBarNavigationAction.aciton()
} }
fun undoLastRound() { fun undoLastRound() {
@@ -77,10 +72,6 @@ class MainViewModel @Inject constructor(
} }
} }
fun clearRedoList() {
redoRounds.clear()
}
fun newGame() { fun newGame() {
viewModelScope.launch { viewModelScope.launch {
redoRounds.clear() redoRounds.clear()

View File

@@ -75,7 +75,7 @@ private val DarkColors = darkColorScheme(
@Composable @Composable
fun AppTheme( fun AppTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(), useDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit content: @Composable () -> Unit
) { ) {
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

View File

@@ -0,0 +1,57 @@
package me.zobrist.tichucounter.ui.about
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.Top
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.BuildConfig
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun AboutView() {
Row(Modifier.padding(20.dp)) {
Image(
modifier = Modifier
.height(80.dp)
.padding(end = 10.dp)
.align(Top),
painter = painterResource(R.drawable.app_logo),
contentDescription = null,
contentScale = ContentScale.Fit
)
Column {
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.headlineMedium
)
Text(text = "V" + BuildConfig.VERSION_NAME)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun AboutViewPreview() {
AppTheme() {
Surface {
AboutView()
}
}
}

View File

@@ -0,0 +1,32 @@
package me.zobrist.tichucounter.ui.composables
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@Composable
fun <T> DropDownMenu(map: Map<T, Int>, selected: T, expanded: Boolean, onSelected: (T?) -> Unit) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { onSelected(null) }
) {
map.forEach {
DropdownMenuItem(
onClick = {
onSelected(it.key)
},
trailingIcon = {
if (it.key == selected) {
Icon(Icons.Outlined.Check, null)
}
},
text = { Text(stringResource(it.value)) },
)
}
}
}

View File

@@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@@ -49,23 +49,12 @@ fun Landscape(viewModel: ICounterViewModel) {
Modifier.weight(1f) Modifier.weight(1f)
) )
} }
if (!viewModel.keyboardHidden) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
KeyboardView( KeyBoardView(viewModel = viewModel)
viewModel.currentScoreA, }
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.enableSubmit,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() }
) { viewModel.submitClicked() }
} }
} }
} }
@Composable @Composable
@@ -89,18 +78,9 @@ fun Portrait(viewModel: ICounterViewModel) {
Modifier.weight(1f) Modifier.weight(1f)
) )
KeyboardView( if (!viewModel.keyboardHidden) {
viewModel.currentScoreA, KeyBoardView(viewModel = viewModel)
viewModel.currentScoreB, }
viewModel.requestFocusA,
viewModel.enableSubmit,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() }
) { viewModel.submitClicked() }
} }
} }
@@ -127,10 +107,12 @@ internal class PreviewViewModel : ICounterViewModel {
override var isAFocused: Boolean = false override var isAFocused: Boolean = false
override var isBFocused: Boolean = false override var isBFocused: Boolean = false
override var requestFocusA: FocusRequester = FocusRequester() override var requestFocusA: FocusRequester = FocusRequester()
override var requestFocusB: FocusRequester = FocusRequester()
override var activeValue: String = currentScoreA override var activeValue: String = currentScoreA
override var inactiveValue: String = currentScoreB override var inactiveValue: String = currentScoreB
override var keyboardHidden: Boolean = false
override fun giveFocusToAIfNone() { override fun focusLastInput() {
} }
override fun updateOtherScore() { override fun updateOtherScore() {
@@ -170,4 +152,13 @@ internal class PreviewViewModel : ICounterViewModel {
override fun updateFocusStateB(state: Boolean) { override fun updateFocusStateB(state: Boolean) {
} }
override fun swapInputScores() {
}
override fun hideKeyboard() {
}
override fun showKeyboard() {
}
} }

View File

@@ -8,29 +8,28 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.data.Round
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject import javax.inject.Inject
interface ICounterViewModel { private enum class Focused { TEAM_A, TEAM_B }
var roundScoreList: List<Round>
var totalScoreA: Int
var totalScoreB: Int
var teamNameA: String
var teamNameB: String
var currentScoreA: String
var currentScoreB: String
var enableSubmit: Boolean
var isAFocused: Boolean
var isBFocused: Boolean
var requestFocusA: FocusRequester
var activeValue: String
var inactiveValue: String
fun giveFocusToAIfNone() interface IKeyBoardViewModel {
val currentScoreA: String
val currentScoreB: String
val enableSubmit: Boolean
val isAFocused: Boolean
val isBFocused: Boolean
val requestFocusA: FocusRequester
val requestFocusB: FocusRequester
val activeValue: String
val inactiveValue: String
val keyboardHidden: Boolean
fun focusLastInput()
fun updateOtherScore() fun updateOtherScore()
fun isValidTichuRound(): Boolean fun isValidTichuRound(): Boolean
fun updateSubmitButton() fun updateSubmitButton()
@@ -39,41 +38,69 @@ interface ICounterViewModel {
fun negateClicked() fun negateClicked()
fun addSub100Clicked(toAdd: Int) fun addSub100Clicked(toAdd: Int)
fun deleteClicked() fun deleteClicked()
fun updateNameA(value: String)
fun updateNameB(value: String)
fun updateFocusStateA(state: Boolean) fun updateFocusStateA(state: Boolean)
fun updateFocusStateB(state: Boolean) fun updateFocusStateB(state: Boolean)
fun swapInputScores()
fun hideKeyboard()
fun showKeyboard()
}
interface ICounterViewModel : IKeyBoardViewModel {
val roundScoreList: List<Round>
val totalScoreA: Int
val totalScoreB: Int
val teamNameA: String
val teamNameB: String
fun updateNameA(value: String)
fun updateNameB(value: String)
} }
@HiltViewModel @HiltViewModel
class CounterViewModel @Inject constructor( class CounterViewModel @Inject constructor(
private val gameRepository: GameRepository, private val gameRepository: GameRepository
private val roundDao: RoundDao,
private val gameDao: GameDao
) : ) :
ViewModel(), ICounterViewModel { ViewModel(), ICounterViewModel {
override var roundScoreList by mutableStateOf(emptyList<Round>()) override var roundScoreList by mutableStateOf(emptyList<Round>())
private set
override var totalScoreA by mutableStateOf(0) override var totalScoreA by mutableStateOf(0)
private set
override var totalScoreB by mutableStateOf(0) override var totalScoreB by mutableStateOf(0)
private set
override var teamNameA by mutableStateOf("") override var teamNameA by mutableStateOf("")
private set
override var teamNameB by mutableStateOf("") override var teamNameB by mutableStateOf("")
private set
override var currentScoreA by mutableStateOf("") override var currentScoreA by mutableStateOf("")
private set
override var currentScoreB by mutableStateOf("") override var currentScoreB by mutableStateOf("")
private set
override var enableSubmit by mutableStateOf(false) override var enableSubmit by mutableStateOf(false)
private set
override var isAFocused by mutableStateOf(false) override var isAFocused by mutableStateOf(false)
private set
override var isBFocused by mutableStateOf(false) override var isBFocused by mutableStateOf(false)
private set
override var requestFocusA by mutableStateOf(FocusRequester()) override var requestFocusA by mutableStateOf(FocusRequester())
private set
override var requestFocusB by mutableStateOf(FocusRequester())
private set
override var keyboardHidden by mutableStateOf(false)
private set
override var activeValue: String override var activeValue: String
get() { get() {
@@ -107,33 +134,31 @@ class CounterViewModel @Inject constructor(
} }
} }
private var lastFocused = Focused.TEAM_A
init { init {
viewModelScope.launch { viewModelScope.launch {
roundDao.getForActiveGame().collect { gameRepository.getActiveGameFlow().collect {
roundScoreList = it
}
}
viewModelScope.launch {
gameDao.getActive().collect {
if (it != null) { if (it != null) {
teamNameA = it.nameA
teamNameB = it.nameB val score = it.getTotalPoints()
roundScoreList = it.rounds
totalScoreA = score.first
totalScoreB = score.second
teamNameA = it.game.nameA
teamNameB = it.game.nameB
}
} }
} }
} }
viewModelScope.launch { override fun focusLastInput() {
roundDao.getRoundSumForActiveGame().collect { score -> when (lastFocused) {
totalScoreA = score.scoreA Focused.TEAM_A -> if (!isAFocused) requestFocusA.requestFocus()
totalScoreB = score.scoreB Focused.TEAM_B -> if (!isBFocused) requestFocusB.requestFocus()
}
}
}
override fun giveFocusToAIfNone() {
if (!isAFocused && !isBFocused) {
requestFocusA.requestFocus()
} }
} }
@@ -175,7 +200,7 @@ class CounterViewModel @Inject constructor(
} }
override fun digitClicked(digit: String) { override fun digitClicked(digit: String) {
giveFocusToAIfNone() focusLastInput()
activeValue += digit activeValue += digit
updateOtherScore() updateOtherScore()
@@ -183,7 +208,7 @@ class CounterViewModel @Inject constructor(
} }
override fun negateClicked() { override fun negateClicked() {
giveFocusToAIfNone() focusLastInput()
activeValue = if (activeValue.contains("-")) { activeValue = if (activeValue.contains("-")) {
activeValue.replace("-", "") activeValue.replace("-", "")
@@ -195,7 +220,7 @@ class CounterViewModel @Inject constructor(
} }
override fun addSub100Clicked(toAdd: Int) { override fun addSub100Clicked(toAdd: Int) {
giveFocusToAIfNone() focusLastInput()
activeValue = try { activeValue = try {
val temp = activeValue.toInt() + toAdd val temp = activeValue.toInt() + toAdd
@@ -236,9 +261,29 @@ class CounterViewModel @Inject constructor(
override fun updateFocusStateA(state: Boolean) { override fun updateFocusStateA(state: Boolean) {
isAFocused = state isAFocused = state
if (state) {
lastFocused = Focused.TEAM_A
}
} }
override fun updateFocusStateB(state: Boolean) { override fun updateFocusStateB(state: Boolean) {
isBFocused = state isBFocused = state
if (state) {
lastFocused = Focused.TEAM_B
}
}
override fun swapInputScores() {
val swap = currentScoreA
currentScoreA = currentScoreB
currentScoreB = swap
}
override fun hideKeyboard() {
keyboardHidden = true
}
override fun showKeyboard() {
keyboardHidden = false
} }
} }

View File

@@ -1,72 +1,107 @@
package me.zobrist.tichucounter.ui.counter package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace import androidx.compose.material.icons.outlined.Backspace
import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.KeyboardHide
import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@Composable
fun KeyBoardView(viewModel: IKeyBoardViewModel) {
KeyboardView(
viewModel.currentScoreA,
viewModel.currentScoreB,
viewModel.requestFocusA,
viewModel.requestFocusB,
viewModel.enableSubmit,
viewModel.isAFocused,
viewModel.isBFocused,
{ viewModel.updateFocusStateA(it) },
{ viewModel.updateFocusStateB(it) },
{ viewModel.digitClicked(it) },
{ viewModel.addSub100Clicked(it) },
{ viewModel.deleteClicked() },
{ viewModel.negateClicked() },
{ viewModel.submitClicked() },
{ viewModel.hideKeyboard() },
{ viewModel.swapInputScores() }
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun KeyboardView( fun KeyboardView(
scoreA: String, scoreA: String,
scoreB: String, scoreB: String,
requestFocus: FocusRequester, requestFocusA: FocusRequester,
requestFocusB: FocusRequester,
enableSubmit: Boolean, enableSubmit: Boolean,
focusStateA: Boolean,
focusStateB: Boolean,
updateFocusStateA: (Boolean) -> Unit, updateFocusStateA: (Boolean) -> Unit,
updateFocusStateB: (Boolean) -> Unit, updateFocusStateB: (Boolean) -> Unit,
digitClicked: (String) -> Unit, digitClicked: (String) -> Unit,
addSub100Clicked: (Int) -> Unit, addSub100Clicked: (Int) -> Unit,
deleteClicked: () -> Unit, deleteClicked: () -> Unit,
negateClicked: () -> Unit, negateClicked: () -> Unit,
submitClicked: () -> Unit submitClicked: () -> Unit,
hideKeyboardClicked: () -> Unit,
onSwapClicked: () -> Unit
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current
Column { Column {
Row { Row(Modifier.height(IntrinsicSize.Max)) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
CenteredTextField( CenteredTextField(
scoreA, scoreA,
"0", "0",
Modifier focusStateA,
.focusRequester(requestFocus) requestFocusA
.onFocusChanged { ) { updateFocusStateA(it.isFocused) }
keyboardController?.hide()
updateFocusStateA(it.isFocused)
}
)
} }
Surface(
Modifier
.wrapContentWidth()
.fillMaxHeight(),
tonalElevation = 3.dp,
shape = MaterialTheme.shapes.extraSmall
) {
Column {
IconButton(onClick = onSwapClicked) {
Icon(Icons.Outlined.SwapHoriz, null)
}
}
}
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
CenteredTextField( CenteredTextField(
scoreB, scoreB,
"0", "0",
Modifier focusStateB,
.onFocusChanged { requestFocusB
keyboardController?.hide() ) {
updateFocusStateB(it.isFocused) updateFocusStateB(it.isFocused)
} }
)
} }
} }
Row { Row {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
KeyboardTextButton("1") { KeyboardTextButton("1") {
@@ -137,8 +172,8 @@ fun KeyboardView(
Row { Row {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
KeyboardTextButton("+/-") { KeyboardIconButton(Icons.Outlined.KeyboardHide) {
negateClicked() hideKeyboardClicked()
} }
} }
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
@@ -146,7 +181,13 @@ fun KeyboardView(
digitClicked("0") digitClicked("0")
} }
} }
Column(Modifier.weight(2f)) {
Column(Modifier.weight(1f)) {
KeyboardTextButton("+/-") {
negateClicked()
}
}
Column(Modifier.weight(1f)) {
KeyboardIconButton(Icons.Outlined.Check, enableSubmit) { KeyboardIconButton(Icons.Outlined.Check, enableSubmit) {
submitClicked() submitClicked()
} }
@@ -157,10 +198,24 @@ fun KeyboardView(
@Composable @Composable
fun KeyboardTextButton(text: String, onClicked: () -> Unit) { fun KeyboardTextButton(text: String, onClicked: () -> Unit) {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val style = if (screenWidth < 350.dp) {
MaterialTheme.typography.labelSmall
} else {
MaterialTheme.typography.labelLarge
}
ElevatedButton( ElevatedButton(
modifier = Modifier.fillMaxWidth().height(50.dp).padding(2.dp), modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(2.dp),
onClick = { onClicked() }, onClick = { onClicked() },
) { Text(text) } ) { Text(text, style = style) }
} }
@Composable @Composable
@@ -168,7 +223,10 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: ()
ElevatedButton( ElevatedButton(
onClick = { onClicked() }, onClick = { onClicked() },
modifier = Modifier.fillMaxWidth().height(50.dp).padding(2.dp), modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(2.dp),
enabled = enabled, enabled = enabled,
) { ) {
Icon( Icon(
@@ -183,23 +241,63 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: ()
fun CenteredTextField( fun CenteredTextField(
value: String, value: String,
placeholder: String, placeholder: String,
modifier: Modifier, focused: Boolean,
focusRequester: FocusRequester? = null,
onFocusStateChanged: (FocusState) -> Unit
) { ) {
val modifier = if (focusRequester != null) {
Modifier.focusRequester(focusRequester)
} else {
Modifier
}
Box(contentAlignment = Alignment.Center) {
TextField( TextField(
value = value, value = value,
onValueChange = { }, onValueChange = { },
placeholder = { placeholder = {
if (!focused) {
Text( Text(
placeholder, placeholder,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}
}, },
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
singleLine = true, singleLine = true,
readOnly = true, readOnly = true,
modifier = modifier.fillMaxWidth() modifier = modifier
.fillMaxWidth()
.onFocusChanged {
onFocusStateChanged(it)
}
) )
if (focused) {
val cursorColor = MaterialTheme.colorScheme.onSurface
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
0f,
cursorColor.alpha,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
)
)
Row {
Text(text = value, color = cursorColor.copy(alpha = 0f))
Divider(
modifier = Modifier
.padding(start = 3.dp, top = 15.dp, bottom = 15.dp)
.width(1.dp)
.fillMaxHeight(),
color = cursorColor.copy(alpha = alpha)
)
}
}
}
} }
@@ -210,17 +308,22 @@ fun KeyboardViewPreview() {
AppTheme { AppTheme {
Surface { Surface {
KeyboardView( KeyboardView(
"", "1",
"350", "3511",
FocusRequester(), FocusRequester(),
false, FocusRequester(),
{}, enableSubmit = false,
{}, focusStateA = true,
{}, focusStateB = false,
{}, updateFocusStateA = {},
{}, updateFocusStateB = {},
{}, digitClicked = {},
{}) addSub100Clicked = {},
deleteClicked = {},
negateClicked = {},
submitClicked = {},
hideKeyboardClicked = {},
onSwapClicked = {})
} }
} }
} }

View File

@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -18,7 +17,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@Composable @Composable
@@ -28,7 +27,7 @@ fun RoundListView(rounds: List<Round>, modifier: Modifier) {
LazyColumn(state = lazyListState, modifier = modifier) { LazyColumn(state = lazyListState, modifier = modifier) {
itemsIndexed(rounds) { index, item -> itemsIndexed(rounds) { index, item ->
RoundListItem(item, index, lazyListState) RoundListItem(item, index)
} }
scope.launch { scope.launch {
@@ -38,7 +37,7 @@ fun RoundListView(rounds: List<Round>, modifier: Modifier) {
} }
@Composable @Composable
private fun RoundListItem(round: Round, index: Int, lazyListState: LazyListState) { private fun RoundListItem(round: Round, index: Int) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -51,7 +50,7 @@ private fun RoundListItem(round: Round, index: Int, lazyListState: LazyListState
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Text( Text(
text = index.toString(), text = (index + 1).toString(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = TextAlign.Center textAlign = TextAlign.Center

View File

@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -18,13 +19,19 @@ fun TeamNamesView(
updateA: (String) -> Unit, updateA: (String) -> Unit,
updateB: (String) -> Unit updateB: (String) -> Unit
) { ) {
Row() {
val color = TextFieldDefaults.textFieldColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
Row {
TextField( TextField(
value = nameA, value = nameA,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateA(it) }, onValueChange = { updateA(it) },
singleLine = true, singleLine = true,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
colors = color
) )
TextField( TextField(
@@ -32,7 +39,8 @@ fun TeamNamesView(
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
onValueChange = { updateB(it) }, onValueChange = { updateB(it) },
singleLine = true, singleLine = true,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
colors = color
) )
} }
} }

View File

@@ -4,10 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -19,8 +16,8 @@ import me.zobrist.tichucounter.ui.AppTheme
@Composable @Composable
fun TeamScoresView(scoreA: Int, scoreB: Int) { fun TeamScoresView(scoreA: Int, scoreB: Int) {
ElevatedCard() { ElevatedCard(elevation = CardDefaults.elevatedCardElevation(3.dp)) {
Row() { Row {
Text( Text(
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
text = scoreA.toString(), text = scoreA.toString(),

View File

@@ -1,78 +1,212 @@
package me.zobrist.tichucounter.ui.history package me.zobrist.tichucounter.ui.history
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card import androidx.compose.material.icons.Icons
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material3.Text import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.runtime.Composable import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.data.GameAndScore import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.ui.composables.DropDownMenu
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@Composable @Composable
fun HistoryList(viewModel: HistoryViewModel) { fun HistoryList(
HistoryList(viewModel.gameAndHistory) viewModel: HistoryViewModel,
navigateToCalculator: () -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
DeleteConfirmDialog(showDeleteDialog) {
showDeleteDialog = false
if (it) {
viewModel.deleteAllInactiveGames()
}
}
HistoryList(
viewModel.gameAndHistory,
{
viewModel.activateGame(it)
navigateToCalculator()
},
{ viewModel.deleteGame(it) },
{ showDeleteDialog = true },
)
}
@Preview
@Composable
fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {}) {
if (show) {
AlertDialog(
onDismissRequest = { onExecuted(false) },
dismissButton = {
TextButton({ onExecuted(false) })
{
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton({ onExecuted(true) })
{
Text(stringResource(R.string.ok))
}
},
title = { Text(stringResource(R.string.delete_inactive_title)) },
text = { Text(stringResource(R.string.delete_inactive_text)) },
)
}
} }
@Composable @Composable
fun HistoryList(games: List<GameAndScore>) { fun HistoryList(
games: List<GameWithScores>,
onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit,
onDeleteAllClicked: () -> Unit
) {
Row {
LazyColumn { LazyColumn {
items(games) { item {
HistoryListItem(it) Text(
modifier = Modifier.padding(start = 10.dp, end = 10.dp),
text = stringResource(R.string.active),
style = MaterialTheme.typography.headlineSmall
)
}
items(games.filter { it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
}
if (games.count() > 1) {
item {
Text(
modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp),
text = stringResource(R.string.inactive),
style = MaterialTheme.typography.headlineSmall
)
}
items(games.filter { !it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
}
item {
Button(
enabled = games.count() > 1,
modifier = Modifier
.padding(start = 4.dp, end = 4.dp, top = 10.dp)
.align(CenterVertically)
.fillMaxWidth(),
onClick = { onDeleteAllClicked() }) {
Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null)
Text(text = stringResource(id = R.string.deleteAll))
}
}
}
} }
} }
} }
@Composable @Composable
fun HistoryListItem(game: GameAndScore) { fun HistoryListItem(
game: GameWithScores,
onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit
) {
val format = val format =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
val cardColor = if (game.game.active) {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
} else {
CardDefaults.cardColors()
}
val totalScores = game.getTotalPoints()
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 4.dp), .padding(all = 4.dp)
.clickable { onOpenClicked(game.game.uid) },
colors = cardColor
) { ) {
Column( Row(
Modifier Modifier
.padding(all = 12.dp), .padding(all = 12.dp)
) { ) {
Row() { Column(Modifier.weight(4f)) {
Text( Text(
text = game.nameA, text = game.game.nameA + " vs " + game.game.nameB,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
Text( Text(
text = game.scoreA.toString(), text = totalScores.first.toString() + " : " + totalScores.second.toString(),
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.bodyLarge
) )
} Spacer(modifier = Modifier.padding(5.dp))
Row() {
Text( Text(
text = game.nameB, text = format.format(game.game.modified),
style = MaterialTheme.typography.headlineSmall
)
Text(
text = game.scoreB.toString(),
style = MaterialTheme.typography.headlineSmall
)
}
Row() {
Text(
text = format.format(game.modified),
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
} }
Column(
Modifier
.wrapContentSize()
.width(40.dp)
) {
if (!game.game.active) {
var expanded by remember { mutableStateOf(false) }
Icon(
modifier = Modifier
.padding(start = 20.dp, bottom = 20.dp)
.clickable { expanded = true },
imageVector = Icons.Outlined.MoreVert,
contentDescription = null
)
DropDownMenu(
mapOf("delete" to R.string.delete),
"",
expanded,
) {
expanded = false
it?.let {
when (it) {
"delete" -> onDeleteClicked(game.game.uid)
}
}
}
}
}
} }
} }
} }
@@ -80,12 +214,27 @@ fun HistoryListItem(game: GameAndScore) {
@Preview @Preview
@Composable @Composable
private fun HistoryListPreview() { private fun HistoryListPreview() {
val tempData = listOf<GameAndScore>( val tempData = listOf(
GameAndScore(false, "abc", "def", Date(), Date(), 1, 10, 50), GameWithScores(
GameAndScore(true, "ADTH", "dogfg", Date(), Date(), 2, 20, 60), Game(true, "abc", "def", Date(), Date()),
GameAndScore(false, "TeamA3", "TeamB3", Date(), Date(), 3, 30, 70), listOf(Round(1, 550, 500))
GameAndScore(false, "TeamA4", "TeamB4", Date(), Date(), 4, 40, 80), ),
GameAndScore(false, "TeamA5", "TeamB5", Date(), Date(), 5, 50, 90) GameWithScores(
Game(false, "ADTH", "dogfg", Date(), Date()),
listOf(Round(2, 20, 60))
),
GameWithScores(
Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()),
listOf(Round(3, 30, 70))
),
GameWithScores(
Game(false, "TeamA4", "TeamB4", Date(), Date()),
listOf(Round(4, 40, 80))
),
GameWithScores(
Game(false, "TeamA5", "TeamB5", Date(), Date()),
listOf(Round(5, 50, 90))
) )
HistoryList(tempData) )
HistoryList(tempData, {}, {}) {}
} }

View File

@@ -7,24 +7,45 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.GameAndScore import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HistoryViewModel @Inject constructor( class HistoryViewModel @Inject constructor(
private val gameDao: GameDao private val gameRepository: GameRepository
) : ViewModel() { ) : ViewModel() {
var gameAndHistory by mutableStateOf(emptyList<GameAndScore>()) var gameAndHistory by mutableStateOf(emptyList<GameWithScores>())
private set private set
init { init {
viewModelScope.launch { viewModelScope.launch {
gameDao.getAllWithPoints().collect { games ->
gameAndHistory = games gameRepository.getAllWithRoundFlow().collect { games ->
gameAndHistory =
games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed()
} }
} }
} }
fun deleteGame(gameId: Long) {
viewModelScope.launch {
gameRepository.deleteGame(gameId)
}
}
fun activateGame(gameId: Long) {
viewModelScope.launch {
gameRepository.setActive(gameId)
}
}
fun deleteAllInactiveGames() {
viewModelScope.launch {
gameRepository.deleteAllInactive()
}
}
} }

View File

@@ -0,0 +1,64 @@
package me.zobrist.tichucounter.ui.layout
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.domain.*
import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.counter.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(
drawerItems: List<DrawerItem>,
selectedDrawerItem: DrawerItem,
onElementClicked: (Route) -> Unit
) {
ModalDrawerSheet {
Text(
modifier = Modifier.padding(start = 10.dp, top = 10.dp),
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall
)
Divider(modifier = Modifier.padding(10.dp))
drawerItems.forEach { screen ->
NavigationDrawerItem(
icon = { Icon(screen.menuIcon, contentDescription = null) },
label = { Text(screen.menuName) },
selected = screen == selectedDrawerItem,
onClick = { onElementClicked(screen.route) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun DrawerContentPreview() {
val counter = DrawerItem(Route.COUNTER, Icons.Outlined.Calculate, "Counter")
val history = DrawerItem(Route.HISTORY, Icons.Outlined.List, "History")
val settings = DrawerItem(Route.SETTINGS, Icons.Outlined.Settings, "Settings")
AppTheme {
Surface {
DrawerContent(
listOf(counter, history, settings),
counter
) {}
}
}
}

View File

@@ -0,0 +1,56 @@
package me.zobrist.tichucounter.ui.layout
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.domain.TopBarState
@Composable
fun TopBar(topBarState: TopBarState) {
TopBar(
topBarState.title,
topBarState.icon,
topBarState.onNavigate,
topBarState.actions
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopBar(
title: String,
icon: ImageVector,
navigateAction: () -> Unit,
actions: List<TopBarAction>
) {
TopAppBar(
title = {
Text(
title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { navigateAction() }) {
Icon(
imageVector = icon,
contentDescription = "Localized description"
)
}
},
actions = {
actions.forEach {
IconButton(onClick = { it.action() }, enabled = it.isActive) {
Icon(
imageVector = it.imageVector,
contentDescription = null,
)
it.composeCode()
}
}
}
)
}

View File

@@ -2,10 +2,12 @@ package me.zobrist.tichucounter.ui.settings
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Alignment.Companion.End
@@ -15,12 +17,15 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.zobrist.tichucounter.R import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.domain.Theme
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.composables.DropDownMenu
val languageMap = mapOf( val languageMap = mapOf(
Language.DEFAULT to R.string.android_default_text,
Language.ENGLISH to R.string.english, Language.ENGLISH to R.string.english,
Language.GERMAN to R.string.german Language.GERMAN to R.string.german
) )
@@ -35,7 +40,7 @@ val themeMap = mapOf(
@Composable @Composable
fun SettingsView(viewModel: SettingsViewModel) { fun SettingsView(viewModel: SettingsViewModel) {
SettingsView( SettingsView(
viewModel.screenOn, viewModel.screenOn.value,
viewModel.language, viewModel.language,
viewModel.theme, viewModel.theme,
{ viewModel.updateScreenOn(it) }, { viewModel.updateScreenOn(it) },
@@ -48,15 +53,15 @@ fun SettingsView(
valueScreenOn: Boolean = true, valueScreenOn: Boolean = true,
valueLanguage: Language = Language.ENGLISH, valueLanguage: Language = Language.ENGLISH,
valueTheme: Theme = Theme.DARK, valueTheme: Theme = Theme.DARK,
updateScreenOn: (Boolean) -> Unit = {}, updateScreenOn: (KeepScreenOn) -> Unit = {},
updateLanguage: (Language) -> Unit = {}, updateLanguage: (Language) -> Unit = {},
updateTheme: (Theme) -> Unit = {} updateTheme: (Theme) -> Unit = {}
) { ) {
Column() { Column {
BooleanSetting( BooleanSetting(
stringResource(R.string.keep_screen_on), stringResource(R.string.keep_screen_on),
valueScreenOn valueScreenOn
) { updateScreenOn(it) } ) { updateScreenOn(if (it) KeepScreenOn.ON else KeepScreenOn.OFF) }
StringSetting( StringSetting(
stringResource(R.string.choose_language_text), stringResource(R.string.choose_language_text),
@@ -72,7 +77,6 @@ fun SettingsView(
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit) { fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit) {
@@ -116,7 +120,12 @@ fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (
.clickable { expanded = true }) { .clickable { expanded = true }) {
Column(Modifier.weight(5f)) { Column(Modifier.weight(5f)) {
Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis) Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis)
Text(stringResource(map[selected]!!), style = MaterialTheme.typography.labelLarge) map[selected]?.let {
Text(
stringResource(it),
style = MaterialTheme.typography.labelLarge
)
}
} }
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
@@ -127,23 +136,26 @@ fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (
) )
} }
DropdownMenu( DropDownMenu(
expanded = expanded, map,
onDismissRequest = { expanded = false } selected,
expanded,
) { ) {
map.forEach {
DropdownMenuItem(
onClick = {
onSelected(it.key)
expanded = false expanded = false
}, it?.let { onSelected(it) }
text = { Text(stringResource(it.value)) },
trailingIcon = {
if (it.key == selected) {
Icon(Icons.Outlined.Check, contentDescription = null)
} }
})
} }
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun SettingsViewPreview() {
AppTheme {
Surface {
SettingsView()
} }
} }
} }
@@ -151,11 +163,16 @@ fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (
@Preview(name = "Light Mode") @Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable @Composable
fun SettingsViewPreview() { fun StringSettingPreview() {
AppTheme() { AppTheme {
Surface() { Surface {
SettingsView() DropDownMenu(
themeMap,
Theme.LIGHT,
true,
) {}
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.domain.Theme
@@ -20,7 +21,7 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte
var theme by mutableStateOf(settings.theme) var theme by mutableStateOf(settings.theme)
private set private set
var screenOn by mutableStateOf(false) var screenOn by mutableStateOf(settings.keepScreenOn)
private set private set
fun updateLanguage(language: Language) { fun updateLanguage(language: Language) {
@@ -33,7 +34,7 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte
this.theme = settings.theme this.theme = settings.theme
} }
fun updateScreenOn(value: Boolean) { fun updateScreenOn(value: KeepScreenOn) {
settings.setKeepScreenOn(value) settings.setKeepScreenOn(value)
screenOn = settings.keepScreenOn screenOn = settings.keepScreenOn
} }

View File

@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zm3,15c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22,16V4c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zm-11,-4l2.03,2.71L16,11l4,5H8l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2H4V6H2z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6zm16,-4H8c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zm-8,12.5v-9l6,4.5 -6,4.5z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/primaryColor"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/primaryColor"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z" />
</vector>

View File

@@ -1,9 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="135"
android:centerColor="#009688"
android:endColor="#00695C"
android:startColor="#4DB6AC"
android:type="linear" />
</shape>

View File

@@ -1,29 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="clear">Neues Spiel starten</string>
<string name="undo">Letzte Runde löschen</string>
<string name="choose_theme_text">Theme auswählen</string> <string name="choose_theme_text">Theme auswählen</string>
<string name="keep_screen_on">Bildschirm eingeschaltet lassen</string> <string name="keep_screen_on">Bildschirm eingeschaltet lassen</string>
<string name="confirmNew">Möchten Sie wirklich ein neues Spiel starten?</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="back">Zurück</string>
<string name="choose_language_text">Sprache wählen</string> <string name="choose_language_text">Sprache wählen</string>
<string name="android_default_text">Android Standard</string> <string name="android_default_text">Android Standard</string>
<string name="english">Englisch</string> <string name="english">Englisch</string>
<string name="german">Deutsch</string> <string name="german">Deutsch</string>
<string name="light">Hell</string> <string name="light">Hell</string>
<string name="dark">Dunkel</string> <string name="dark">Dunkel</string>
<string name="settings">Einstellungen</string>
<string name="display">Anzeige</string>
<string name="activate">Aktivieren</string>
<string name="delete">Löschen</string>
<string name="menu_history">Verlauf</string> <string name="menu_history">Verlauf</string>
<string name="menu_settings">Einstellungen</string> <string name="menu_settings">Einstellungen</string>
<string name="menu_counter">Counter</string>
<string name="submit">Übermitteln</string>
<string name="on">Ein</string> <string name="on">Ein</string>
<string name="off">Aus</string> <string name="off">Aus</string>
<string name="newGame">Neues Spiel</string> <string name="newGame">Neues Spiel</string>
<string name="delete_inactive_title">Verlauf löschen</string>
<string name="delete_inactive_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
<string name="ok">Ok</string>
<string name="delete">Löschen</string>
<string name="deleteAll">Alle löschen</string>
<string name="active">Aktives Spiel</string>
<string name="inactive">Vergangene Spiele</string>
<string name="menu_counter">Counter</string>
</resources> </resources>

View File

@@ -1,3 +0,0 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="primaryColor">#d50000</color> <color name="primaryColor">#d50000</color>
<color name="primaryLightColor">#ff5131</color>
<color name="primaryDarkColor">#9b0000</color>
<color name="secondaryColor">#ffccbc</color>
<color name="secondaryLightColor">#ffffee</color>
<color name="secondaryDarkColor">#cb9b8c</color>
<color name="primaryTextColor">#ffffff</color>
<color name="secondaryTextColor">#000000</color>
</resources> </resources>

View File

@@ -1,9 +0,0 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources>

View File

@@ -1,8 +0,0 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<item name="ic_menu_camera" type="drawable">@android:drawable/ic_menu_camera</item>
<item name="ic_menu_gallery" type="drawable">@android:drawable/ic_menu_gallery</item>
<item name="ic_menu_slideshow" type="drawable">@android:drawable/ic_menu_slideshow</item>
<item name="ic_menu_manage" type="drawable">@android:drawable/ic_menu_manage</item>
<item name="ic_menu_share" type="drawable">@android:drawable/ic_menu_share</item>
<item name="ic_menu_send" type="drawable">@android:drawable/ic_menu_send</item>
</resources>

View File

@@ -1,34 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">Tichu Counter</string> <string name="app_name" translatable="false">Tichu Counter</string>
<string name="team_a" translatable="false">Team A</string>
<string name="team_b" translatable="false">Team B</string>
<string name="title_activity_settings" translatable="false">SettingsActivity</string>
<string name="clear">Start new game</string>
<string name="undo">Undo last round</string>
<string name="choose_theme_text">Choose theme</string> <string name="choose_theme_text">Choose theme</string>
<string name="keep_screen_on">Keep screen on</string> <string name="keep_screen_on">Keep screen on</string>
<string name="confirmNew">Do you really want to start a new Game?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="back">Back</string>
<string name="choose_language_text">Choose language</string> <string name="choose_language_text">Choose language</string>
<string name="android_default_text">Android Default</string> <string name="android_default_text">Android Default</string>
<string name="english">English</string> <string name="english">English</string>
<string name="german">German</string> <string name="german">German</string>
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="display">Display</string>
<string name="settings">Settings</string>
<string name="menu_counter">Counter</string>
<string name="menu_history">History</string> <string name="menu_history">History</string>
<string name="menu_settings">Settings</string> <string name="menu_settings">Settings</string>
<string name="activate">Activate</string>
<string name="delete">Delete</string>
<string name="submit">Submit</string>
<string name="on">On</string> <string name="on">On</string>
<string name="off">Off</string> <string name="off">Off</string>
<string name="newGame">New Game</string> <string name="newGame">New Game</string>
<string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="delete">Delete</string>
<string name="deleteAll">Delete all</string>
<string name="active">Current Game</string>
<string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
</resources> </resources>

View File

@@ -1,8 +1,5 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"></style> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
</resources> </resources>