diff --git a/app/build.gradle b/app/build.gradle index 814c627..6628138 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ def versionPropertiesFile = rootProject.file("version.properties") def keystoreProperties = new Properties() def versionProperties = new Properties() -def versionMajor = 1 -def versionMinor = 1 +def versionMajor = 2 +def versionMinor = 0 // Load your keystore.properties file into the keystoreProperties object. keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) diff --git a/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json index 992f833..324b84a 100644 --- a/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json +++ b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "f07e88c78e54c69c73890495a2121bf4", + "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)", + "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", @@ -30,7 +30,7 @@ "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", - "notNull": false + "notNull": true } ], "primaryKey": { @@ -44,7 +44,7 @@ }, { "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)", + "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", @@ -80,7 +80,7 @@ "fieldPath": "uid", "columnName": "uid", "affinity": "INTEGER", - "notNull": false + "notNull": true } ], "primaryKey": { @@ -96,7 +96,7 @@ "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, 'f07e88c78e54c69c73890495a2121bf4')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1739540cd7d5436941316932a1036d83')" ] } } \ No newline at end of file diff --git a/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json new file mode 100644 index 0000000..914f81c --- /dev/null +++ b/app/schemas/me.zobrist.tichucounter.data.AppDatabase/2.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/BaseActivity.kt b/app/src/main/java/me/zobrist/tichucounter/BaseActivity.kt deleted file mode 100644 index e9a040b..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/BaseActivity.kt +++ /dev/null @@ -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 != currentLocale) { - val newLocale = LocaleListCompat.forLanguageTags(language.value) - AppCompatDelegate.setApplicationLocales(newLocale) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 42bc7c8..b964c6c 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -1,9 +1,11 @@ package me.zobrist.tichucounter import android.os.Bundle +import android.view.WindowManager import androidx.activity.compose.setContent 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.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -11,38 +13,34 @@ import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector 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.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import me.zobrist.tichucounter.domain.NavigationAction -import me.zobrist.tichucounter.domain.TopBarAction -import me.zobrist.tichucounter.repository.GameRepository +import me.zobrist.tichucounter.domain.* import me.zobrist.tichucounter.ui.AppTheme 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.history.HistoryList 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.SettingsViewModel import javax.inject.Inject @AndroidEntryPoint -class MainActivity : BaseActivity() { +class MainActivity : AppCompatActivity(), ISettingsChangeListener { @Inject - lateinit var gameRepository: GameRepository + lateinit var settingsAdapter: SettingsAdapter private val counterViewModel: CounterViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels() @@ -53,6 +51,8 @@ class MainActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + settingsAdapter.registerOnChangeListener(this) + setContent { AppTheme { val systemUiController = rememberSystemUiController() @@ -62,6 +62,86 @@ class MainActivity : BaseActivity() { } } + override fun onDestroy() { + super.onDestroy() + settingsAdapter.unregisterOnChangeListener(this) + } + + override fun onLanguageChanged(language: Language) { + AppCompatDelegate.setApplicationLocales(language.value) + } + + override fun onThemeChanged(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 + } + 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) + @Composable + private fun NavigationDrawer() { + val drawerState = rememberDrawerState(DrawerValue.Closed) + val scope = rememberCoroutineScope() + val navController = rememberNavController() + + val items = listOf( + 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 currentDestination = + Route.valueOf(navBackStackEntry?.destination?.route ?: Route.COUNTER.name) + + ModalNavigationDrawer( + drawerState = drawerState, + gesturesEnabled = drawerState.isOpen, + drawerContent = { + DrawerContent( + drawerItems = items, + selectedDrawerItem = items.first { it.route == currentDestination }) { + scope.launch { + drawerState.close() + + } + navController.navigate(it) + } + } + ) { + MyScaffoldLayout( + drawerState, + scope, + navController, + counterViewModel.keyboardHidden && (currentDestination == Route.COUNTER) + ) { counterViewModel.showKeyboard() } + } + } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyScaffoldLayout( @@ -72,6 +152,8 @@ class MainActivity : BaseActivity() { fabAction: () -> Unit ) { + var topBarState by remember { mutableStateOf(TopBarState()) } + Scaffold( floatingActionButton = { if (showFab) { @@ -81,199 +163,72 @@ class MainActivity : BaseActivity() { } } }, - topBar = { - TopBar( - mainViewModel.topBarTitle, - mainViewModel.topBarIcon, - { mainViewModel.onNavigateClicked() }, - mainViewModel.topBarActions - ) - }) { + topBar = { TopBar(topBarState) }) { paddings -> NavHost( navController = navController, - startDestination = "counter", - modifier = Modifier.padding(it) + startDestination = Route.COUNTER.name, + modifier = Modifier.padding(paddings) ) { - composable("counter") { - Counter(counterViewModel) - mainViewModel.topBarActions = (listOf( - TopBarAction( - Icons.Outlined.Undo, - mainViewModel.isUndoActionActive - ) { mainViewModel.undoLastRound() }, - TopBarAction( - Icons.Outlined.Redo, - mainViewModel.isRedoActionActive - ) { mainViewModel.redoLastRound() } + composable(Route.COUNTER) { - )) - mainViewModel.topBarIcon = Icons.Outlined.Menu - mainViewModel.topBarTitle = stringResource(R.string.app_name) - mainViewModel.topBarNavigationAction = - NavigationAction { scope.launch { drawerState.open() } } - } - composable("history") { + var expanded by remember { mutableStateOf(false) } - var openDialog by remember { mutableStateOf(false) } - - HistoryList(historyViewModel, openDialog, { deleteAll -> - if (deleteAll) { - mainViewModel.deleteAllInactiveGames() - } - openDialog = false - }) { navController.navigate("counter") } - mainViewModel.topBarActions = listOf( - TopBarAction( - Icons.Outlined.DeleteForever, - true - ) { - openDialog = true - } - ) - 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) - @Composable - private fun TopBar( - title: String, - icon: ImageVector, - navigateAction: () -> Unit, - actions: List - ) { - 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, - ) - } - } - } - ) - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - private fun NavigationDrawer() { - val drawerState = rememberDrawerState(DrawerValue.Closed) - val scope = rememberCoroutineScope() - val navController = rememberNavController() - - val items = listOf( - Screen("history", Icons.Outlined.List, R.string.menu_history), - Screen("settings", Icons.Outlined.Settings, R.string.menu_settings) - ) - - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - - ModalNavigationDrawer( - drawerState = drawerState, - gesturesEnabled = false, - drawerContent = { - ModalDrawerSheet { - - 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 - // 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 + 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() + } } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = true } }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) - ) - } + + )) + ) { 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) + } + + composable(Route.ABOUT) { + topBarState = + TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } } + + AboutView() } } - ) { - MyScaffoldLayout( - drawerState, - scope, - navController, - counterViewModel.keyboardHidden - ) { counterViewModel.keyboardHidden = false } } } - - private class Screen(val route: String, val icon: ImageVector, @StringRes val resourceId: Int) } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt b/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt index 431437d..06eca31 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/AppDatabase.kt @@ -3,6 +3,8 @@ package me.zobrist.tichucounter.data import androidx.room.Database import androidx.room.RoomDatabase 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) @TypeConverters(DateConverter::class) diff --git a/app/src/main/java/me/zobrist/tichucounter/data/Game.kt b/app/src/main/java/me/zobrist/tichucounter/data/Game.kt deleted file mode 100644 index 94255ae..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/Game.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameAndScore.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameAndScore.kt deleted file mode 100644 index 658ee50..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameAndScore.kt +++ /dev/null @@ -1,16 +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 \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt index 79d4519..00a9855 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameDao.kt @@ -2,6 +2,7 @@ package me.zobrist.tichucounter.data import androidx.room.* import kotlinx.coroutines.flow.Flow +import me.zobrist.tichucounter.data.entity.Game @Dao @@ -10,19 +11,13 @@ interface GameDao : DaoBase { @Query("SELECT * FROM game") fun getAll(): Flow> - @Query( - "SELECT active, " + - "nameA, " + - "nameB, " + - "created, " + - "modified, " + - "game.uid as gameId, " + - "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> + @Transaction + @Query("SELECT * FROM game where uid ") + fun getGamesWithRounds(): Flow> + + @Transaction + @Query("SELECT * FROM game WHERE active is 1") + fun getActiveWithRounds(): Flow @Query("SELECT * FROM game WHERE uid is :gameId") fun getGameById(gameId: Long): Flow @@ -30,6 +25,7 @@ interface GameDao : DaoBase { @Query("SELECT * FROM game WHERE active is 1") fun getActive(): Flow + @Query("UPDATE game SET active = 1 WHERE uid is :gameId;") fun setActive(gameId: Long) diff --git a/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt new file mode 100644 index 0000000..44d2d54 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/data/GameWithScores.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/IEntity.kt b/app/src/main/java/me/zobrist/tichucounter/data/IEntity.kt deleted file mode 100644 index c01aff7..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/IEntity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package me.zobrist.tichucounter.data - -interface IEntity { - val uid: Long? -} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/IGame.kt b/app/src/main/java/me/zobrist/tichucounter/data/IGame.kt deleted file mode 100644 index 3afbdff..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/IGame.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/IRound.kt b/app/src/main/java/me/zobrist/tichucounter/data/IRound.kt deleted file mode 100644 index 6b62039..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/IRound.kt +++ /dev/null @@ -1,7 +0,0 @@ -package me.zobrist.tichucounter.data - -interface IRound { - var gameId: Long - var scoreA: Int - var scoreB: Int -} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/Round.kt b/app/src/main/java/me/zobrist/tichucounter/data/Round.kt deleted file mode 100644 index 828fbf5..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/data/Round.kt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt b/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt index 4063e76..2da3a09 100644 --- a/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt +++ b/app/src/main/java/me/zobrist/tichucounter/data/RoundDao.kt @@ -1,7 +1,7 @@ package me.zobrist.tichucounter.data import androidx.room.* -import kotlinx.coroutines.flow.Flow +import me.zobrist.tichucounter.data.entity.Round @Dao interface RoundDao : DaoBase { @@ -12,20 +12,4 @@ interface RoundDao : DaoBase { @Query("SELECT * FROM round WHERE gameId is :gameId") fun getAllForGame(gameId: Long?): List - @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" - ) - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - fun getRoundSumForActiveGame(): Flow - - @Query( - "SELECT gameId, scoreA, scoreB, round.uid " + - "FROM round " + - "LEFT JOIN game ON game.uid = round.gameId " + - "WHERE game.active == 1" - ) - fun getForActiveGame(): Flow> } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt new file mode 100644 index 0000000..e70a2de --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/Game.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt new file mode 100644 index 0000000..23b9c73 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/IEntity.kt @@ -0,0 +1,5 @@ +package me.zobrist.tichucounter.data.entity + +interface IEntity { + val uid: Long +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt b/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt new file mode 100644 index 0000000..2e21660 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/data/entity/Round.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt b/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt new file mode 100644 index 0000000..b3757ef --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/DrawerItem.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt b/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt new file mode 100644 index 0000000..5094493 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/GameWithScoresExtension.kt @@ -0,0 +1,14 @@ +package me.zobrist.tichucounter.domain + +import me.zobrist.tichucounter.data.GameWithScores + +fun GameWithScores.getTotalPoints(): Pair { + var scoreA = 0 + var scoreB = 0 + + this.rounds.forEach { + scoreA += it.scoreA + scoreB += it.scoreB + } + return Pair(scoreA, scoreB) +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt new file mode 100644 index 0000000..68771fb --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt @@ -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 = emptyList(), + deepLinks: List = emptyList(), + content: @Composable (NavBackStackEntry) -> Unit +) { + this.composable(route.name, arguments, deepLinks, content) +} diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/NavigationAction.kt b/app/src/main/java/me/zobrist/tichucounter/domain/NavigationAction.kt deleted file mode 100644 index eb9dd5b..0000000 --- a/app/src/main/java/me/zobrist/tichucounter/domain/NavigationAction.kt +++ /dev/null @@ -1,3 +0,0 @@ -package me.zobrist.tichucounter.domain - -class NavigationAction(val action: () -> Unit) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt b/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt new file mode 100644 index 0000000..5ae62a8 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/Route.kt @@ -0,0 +1,3 @@ +package me.zobrist.tichucounter.domain + +enum class Route { COUNTER, HISTORY, SETTINGS, ABOUT } diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index 65bfb82..452e678 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -1,65 +1,116 @@ package me.zobrist.tichucounter.domain import android.content.Context -import androidx.appcompat.app.AppCompatDelegate.getApplicationLocales +import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +import javax.inject.Singleton 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) { private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + private var listenerList = mutableListOf() - val language: Language - get() { - return try { - var setting = sharedPreferences.getString(Language::class.simpleName, null) - enumValueOf(setting!!) - } catch (_: NullPointerException) { - val current = getCurrentAppLanguage() - setLanguage(current) - current - } + var language: Language + private set + + var theme: Theme + private set + + var keepScreenOn: KeepScreenOn + private set + + init { + language = try { + enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!) + } catch (_: NullPointerException) { + Language.DEFAULT } - val theme: Theme - get() { - val setting = sharedPreferences.getString(Theme::class.simpleName, Theme.DEFAULT.name) - return enumValueOf(setting!!) + theme = try { + enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!) + } catch (_: java.lang.Exception) { + Theme.DEFAULT } - val keepScreenOn: Boolean - get() { - return sharedPreferences.getBoolean("keep_screen_on", false) + keepScreenOn = try { + enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!) + } catch (_: java.lang.Exception) { + KeepScreenOn.OFF } + } - private fun getCurrentAppLanguage(): Language { - return when (getApplicationLocales()[0].toString()) { - "de" -> Language.GERMAN - else -> Language.ENGLISH + fun registerOnChangeListener(listener: ISettingsChangeListener) { + listenerList.add(listener) + + listener.onThemeChanged(theme) + listener.onLanguageChanged(language) + listener.onScreenOnChanged(keepScreenOn) + } + + fun unregisterOnChangeListener(listener: ISettingsChangeListener?) { + if (listener != null) { + listenerList.remove(listener) } } fun setLanguage(language: Language) { - val editor = sharedPreferences.edit() - editor.putString(Language::class.simpleName, language.name) - editor.apply() + this.language = language + updatePreference(Language::class.simpleName, language.name) + notifyListeners(language) } fun setTheme(theme: Theme) { + this.theme = theme + updatePreference(Theme::class.simpleName, theme.name) + notifyListeners(theme) + } + + 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() - editor.putString(Theme::class.simpleName, theme.name) + editor.putString(name, value) editor.apply() } - fun setKeepScreenOn(setting: Boolean) { - val editor = sharedPreferences.edit() - editor.putBoolean("keep_screen_on", setting) - 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt index 02ce8c4..0a9eb39 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarAction.kt @@ -1,5 +1,11 @@ package me.zobrist.tichucounter.domain +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector -class TopBarAction(val imageVector: ImageVector, val isActive: Boolean, val action: () -> Unit) \ No newline at end of file +class TopBarAction( + val imageVector: ImageVector, + val isActive: Boolean, + val action: () -> Unit, + val composeCode: @Composable () -> Unit = {} +) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt new file mode 100644 index 0000000..46bbd46 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/TopBarState.kt @@ -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 = emptyList(), + var onNavigate: () -> Unit = {} +) \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt index 8ae41f6..5716266 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -2,13 +2,15 @@ package me.zobrist.tichucounter.repository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.zobrist.tichucounter.data.Game 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.entity.Game +import me.zobrist.tichucounter.data.entity.Round import java.util.* import javax.inject.Inject @@ -81,7 +83,7 @@ class GameRepository @Inject constructor( withContext(Dispatchers.IO) { val active = activeGame active.modified = Date() - val round = Round(active.uid!!, scoreA, scoreB) + val round = Round(active.uid, scoreA, scoreB) roundDao.insert(round) gameDao.update(active) } @@ -90,7 +92,7 @@ class GameRepository @Inject constructor( suspend fun deleteGame(uid: Long) { withContext(Dispatchers.IO) { try { - gameDao.getGameById(uid).take(1).collect() { + gameDao.getGameById(uid).take(1).collect { gameDao.delete(it) val rounds = roundDao.getAllForGame(it.uid) roundDao.delete(rounds) @@ -103,7 +105,7 @@ class GameRepository @Inject constructor( suspend fun deleteAllInactive() { withContext(Dispatchers.IO) { try { - gameDao.getAll().take(1).collect() { games -> + gameDao.getAll().take(1).collect { games -> val activeId = games.first { it.active }.uid val gamesToDelete = games.filter { !it.active } @@ -116,4 +118,12 @@ class GameRepository @Inject constructor( } } } + + fun getActiveGameFlow(): Flow { + return gameDao.getActiveWithRounds() + } + + fun getAllWithRoundFlow(): Flow> { + return gameDao.getGamesWithRounds() + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt b/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt index b073fd2..4dea1c7 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/Color.kt @@ -1,21 +1,21 @@ -@file:Suppress("unused", "unused", "unused") +@file:Suppress("unused") package me.zobrist.tichucounter.ui 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_primaryContainer = Color(0xFFFFDADA) -val md_theme_light_onPrimaryContainer = Color(0xFF40000B) -val md_theme_light_secondary = Color(0xFF6E5D00) +val md_theme_light_primaryContainer = Color(0xFFFFDADB) +val md_theme_light_onPrimaryContainer = Color(0xFF40000F) +val md_theme_light_secondary = Color(0xFF765659) val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFFFE261) -val md_theme_light_onSecondaryContainer = Color(0xFF221B00) -val md_theme_light_tertiary = Color(0xFF76592F) +val md_theme_light_secondaryContainer = Color(0xFFFFDADB) +val md_theme_light_onSecondaryContainer = Color(0xFF2C1517) +val md_theme_light_tertiary = Color(0xFF775930) val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFFFDDB1) -val md_theme_light_onTertiaryContainer = Color(0xFF291800) +val md_theme_light_tertiaryContainer = Color(0xFFFFDDB5) +val md_theme_light_onTertiaryContainer = Color(0xFF2A1800) val md_theme_light_error = Color(0xFFBA1A1A) val md_theme_light_errorContainer = Color(0xFFFFDAD6) val md_theme_light_onError = Color(0xFFFFFFFF) @@ -24,29 +24,29 @@ val md_theme_light_background = Color(0xFFFFFBFF) val md_theme_light_onBackground = Color(0xFF201A1A) val md_theme_light_surface = Color(0xFFFFFBFF) val md_theme_light_onSurface = Color(0xFF201A1A) -val md_theme_light_surfaceVariant = Color(0xFFF4DDDD) -val md_theme_light_onSurfaceVariant = Color(0xFF524343) -val md_theme_light_outline = Color(0xFF857373) -val md_theme_light_inverseOnSurface = Color(0xFFFBEEED) +val md_theme_light_surfaceVariant = Color(0xFFF4DDDE) +val md_theme_light_onSurfaceVariant = Color(0xFF524344) +val md_theme_light_outline = Color(0xFF857374) +val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE) 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_surfaceTint = Color(0xFFBE0034) -val md_theme_light_outlineVariant = Color(0xFFD7C1C1) +val md_theme_light_surfaceTint = Color(0xFF9C404D) +val md_theme_light_outlineVariant = Color(0xFFD7C1C2) val md_theme_light_scrim = Color(0xFF000000) -val md_theme_dark_primary = Color(0xFFFFB3B5) -val md_theme_dark_onPrimary = Color(0xFF680018) -val md_theme_dark_primaryContainer = Color(0xFF920026) -val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADA) -val md_theme_dark_secondary = Color(0xFFE6C500) -val md_theme_dark_onSecondary = Color(0xFF3A3000) -val md_theme_dark_secondaryContainer = Color(0xFF534600) -val md_theme_dark_onSecondaryContainer = Color(0xFFFFE261) -val md_theme_dark_tertiary = Color(0xFFE6C18D) -val md_theme_dark_onTertiary = Color(0xFF422C05) -val md_theme_dark_tertiaryContainer = Color(0xFF5C421A) -val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB1) +val md_theme_dark_primary = Color(0xFFFFB2B9) +val md_theme_dark_onPrimary = Color(0xFF5F1222) +val md_theme_dark_primaryContainer = Color(0xFF7D2937) +val md_theme_dark_onPrimaryContainer = Color(0xFFFFDADB) +val md_theme_dark_secondary = Color(0xFFE5BDBF) +val md_theme_dark_onSecondary = Color(0xFF44292C) +val md_theme_dark_secondaryContainer = Color(0xFF5C3F41) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFDADB) +val md_theme_dark_tertiary = Color(0xFFE8C08E) +val md_theme_dark_onTertiary = Color(0xFF442B06) +val md_theme_dark_tertiaryContainer = Color(0xFF5D411B) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB5) val md_theme_dark_error = Color(0xFFFFB4AB) val md_theme_dark_errorContainer = Color(0xFF93000A) val md_theme_dark_onError = Color(0xFF690005) @@ -55,16 +55,16 @@ val md_theme_dark_background = Color(0xFF201A1A) val md_theme_dark_onBackground = Color(0xFFECE0DF) val md_theme_dark_surface = Color(0xFF201A1A) val md_theme_dark_onSurface = Color(0xFFECE0DF) -val md_theme_dark_surfaceVariant = Color(0xFF524343) -val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C1) -val md_theme_dark_outline = Color(0xFF9F8C8C) +val md_theme_dark_surfaceVariant = Color(0xFF524344) +val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C2) +val md_theme_dark_outline = Color(0xFF9F8C8D) val md_theme_dark_inverseOnSurface = Color(0xFF201A1A) 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_surfaceTint = Color(0xFFFFB3B5) -val md_theme_dark_outlineVariant = Color(0xFF524343) +val md_theme_dark_surfaceTint = Color(0xFFFFB2B9) +val md_theme_dark_outlineVariant = Color(0xFF524344) val md_theme_dark_scrim = Color(0xFF000000) -val seed = Color(0xFFED0043) +val seed = Color(0xFF833842) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index cc08b6b..53fde44 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -1,7 +1,5 @@ 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.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -10,50 +8,47 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import me.zobrist.tichucounter.data.Round -import me.zobrist.tichucounter.data.RoundDao -import me.zobrist.tichucounter.domain.NavigationAction -import me.zobrist.tichucounter.domain.TopBarAction +import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( - private val gameRepository: GameRepository, - roundDao: RoundDao + private val gameRepository: GameRepository ) : ViewModel() { + private var redoRounds = mutableStateListOf() private var expectedRoundCount = 0 - var topBarTitle by mutableStateOf("") - var topBarActions by mutableStateOf(emptyList()) - var topBarIcon by mutableStateOf(Icons.Filled.Menu) var isUndoActionActive by mutableStateOf(false) - var topBarNavigationAction by mutableStateOf(NavigationAction {}) val isRedoActionActive: Boolean get() = redoRounds.isNotEmpty() + var activeGameHasRounds by mutableStateOf(false) + private set + init { viewModelScope.launch { - roundDao.getForActiveGame().collect { - isUndoActionActive = it.isNotEmpty() - if (expectedRoundCount != it.count()) { - redoRounds.clear() + gameRepository.getActiveGameFlow().collect { + + activeGameHasRounds = it?.rounds?.isNotEmpty() == true + + if (it != null) { + isUndoActionActive = it.rounds.isNotEmpty() + + if (expectedRoundCount != it.rounds.count()) { + redoRounds.clear() + } + + expectedRoundCount = it.rounds.count() } - - expectedRoundCount = it.count() } } } - - fun onNavigateClicked() { - topBarNavigationAction.action() - } - fun undoLastRound() { viewModelScope.launch { val round = gameRepository.getLastRound() @@ -83,10 +78,4 @@ class MainViewModel @Inject constructor( gameRepository.newGame() } } - - fun deleteAllInactiveGames() { - viewModelScope.launch { - gameRepository.deleteAllInactive() - } - } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt new file mode 100644 index 0000000..9202b49 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/ui/about/AboutView.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt new file mode 100644 index 0000000..12a9700 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt @@ -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 DropDownMenu(map: Map, 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)) }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index 3a9ea6b..1113d76 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalConfiguration 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 @@ -49,21 +49,10 @@ fun Landscape(viewModel: ICounterViewModel) { Modifier.weight(1f) ) } - - Column(Modifier.weight(1f)) { - KeyboardView( - 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() }, - { viewModel.keyboardHidden = true }) + if (!viewModel.keyboardHidden) { + Column(Modifier.weight(1f)) { + KeyBoardView(viewModel = viewModel) + } } } } @@ -90,19 +79,7 @@ fun Portrait(viewModel: ICounterViewModel) { ) if (!viewModel.keyboardHidden) { - KeyboardView( - 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() }, - { viewModel.keyboardHidden = true }) + KeyBoardView(viewModel = viewModel) } } } @@ -130,11 +107,12 @@ internal class PreviewViewModel : ICounterViewModel { override var isAFocused: Boolean = false override var isBFocused: Boolean = false override var requestFocusA: FocusRequester = FocusRequester() + override var requestFocusB: FocusRequester = FocusRequester() override var activeValue: String = currentScoreA override var inactiveValue: String = currentScoreB override var keyboardHidden: Boolean = false - override fun giveFocusToAIfNone() { + override fun focusLastInput() { } override fun updateOtherScore() { @@ -174,4 +152,13 @@ internal class PreviewViewModel : ICounterViewModel { override fun updateFocusStateB(state: Boolean) { } + override fun swapInputScores() { + } + + override fun hideKeyboard() { + } + + override fun showKeyboard() { + } + } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 74351c2..e6ae019 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -8,30 +8,28 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import me.zobrist.tichucounter.data.GameDao -import me.zobrist.tichucounter.data.Round -import me.zobrist.tichucounter.data.RoundDao +import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.domain.Tichu +import me.zobrist.tichucounter.domain.getTotalPoints import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject -interface ICounterViewModel { - var roundScoreList: List - 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 - var keyboardHidden: Boolean +private enum class Focused { TEAM_A, TEAM_B } - 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 isValidTichuRound(): Boolean fun updateSubmitButton() @@ -40,43 +38,69 @@ interface ICounterViewModel { fun negateClicked() fun addSub100Clicked(toAdd: Int) fun deleteClicked() - fun updateNameA(value: String) - fun updateNameB(value: String) fun updateFocusStateA(state: Boolean) fun updateFocusStateB(state: Boolean) + fun swapInputScores() + fun hideKeyboard() + fun showKeyboard() + +} + +interface ICounterViewModel : IKeyBoardViewModel { + val roundScoreList: List + val totalScoreA: Int + val totalScoreB: Int + val teamNameA: String + val teamNameB: String + + fun updateNameA(value: String) + fun updateNameB(value: String) } @HiltViewModel class CounterViewModel @Inject constructor( - private val gameRepository: GameRepository, - private val roundDao: RoundDao, - private val gameDao: GameDao + private val gameRepository: GameRepository ) : ViewModel(), ICounterViewModel { override var roundScoreList by mutableStateOf(emptyList()) + private set override var totalScoreA by mutableStateOf(0) + private set override var totalScoreB by mutableStateOf(0) + private set override var teamNameA by mutableStateOf("") + private set override var teamNameB by mutableStateOf("") + private set override var currentScoreA by mutableStateOf("") + private set override var currentScoreB by mutableStateOf("") + private set override var enableSubmit by mutableStateOf(false) + private set override var isAFocused by mutableStateOf(false) + private set override var isBFocused by mutableStateOf(false) + private set 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 get() { @@ -110,33 +134,31 @@ class CounterViewModel @Inject constructor( } } + + private var lastFocused = Focused.TEAM_A + init { viewModelScope.launch { - roundDao.getForActiveGame().collect { - roundScoreList = it - } - } - - viewModelScope.launch { - gameDao.getActive().collect { + gameRepository.getActiveGameFlow().collect { if (it != null) { - teamNameA = it.nameA - teamNameB = it.nameB - } - } - } - viewModelScope.launch { - roundDao.getRoundSumForActiveGame().collect { score -> - totalScoreA = score.scoreA - totalScoreB = score.scoreB + val score = it.getTotalPoints() + + roundScoreList = it.rounds + totalScoreA = score.first + totalScoreB = score.second + + teamNameA = it.game.nameA + teamNameB = it.game.nameB + } } } } - override fun giveFocusToAIfNone() { - if (!isAFocused && !isBFocused) { - requestFocusA.requestFocus() + override fun focusLastInput() { + when (lastFocused) { + Focused.TEAM_A -> if (!isAFocused) requestFocusA.requestFocus() + Focused.TEAM_B -> if (!isBFocused) requestFocusB.requestFocus() } } @@ -178,7 +200,7 @@ class CounterViewModel @Inject constructor( } override fun digitClicked(digit: String) { - giveFocusToAIfNone() + focusLastInput() activeValue += digit updateOtherScore() @@ -186,7 +208,7 @@ class CounterViewModel @Inject constructor( } override fun negateClicked() { - giveFocusToAIfNone() + focusLastInput() activeValue = if (activeValue.contains("-")) { activeValue.replace("-", "") @@ -198,7 +220,7 @@ class CounterViewModel @Inject constructor( } override fun addSub100Clicked(toAdd: Int) { - giveFocusToAIfNone() + focusLastInput() activeValue = try { val temp = activeValue.toInt() + toAdd @@ -239,9 +261,29 @@ class CounterViewModel @Inject constructor( override fun updateFocusStateA(state: Boolean) { isAFocused = state + if (state) { + lastFocused = Focused.TEAM_A + } } override fun updateFocusStateB(state: Boolean) { 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 } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt index 28340c0..273cd8b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt @@ -1,34 +1,59 @@ package me.zobrist.tichucounter.ui.counter import android.content.res.Configuration +import androidx.compose.animation.core.* import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Backspace 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.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusState import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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 fun KeyboardView( scoreA: String, scoreB: String, - requestFocus: FocusRequester, + requestFocusA: FocusRequester, + requestFocusB: FocusRequester, enableSubmit: Boolean, + focusStateA: Boolean, + focusStateB: Boolean, updateFocusStateA: (Boolean) -> Unit, updateFocusStateB: (Boolean) -> Unit, digitClicked: (String) -> Unit, @@ -36,39 +61,47 @@ fun KeyboardView( deleteClicked: () -> Unit, negateClicked: () -> Unit, submitClicked: () -> Unit, - hideKeyboardClicked: () -> Unit + hideKeyboardClicked: () -> Unit, + onSwapClicked: () -> Unit ) { - val keyboardController = LocalSoftwareKeyboardController.current - Column { - Row { + Row(Modifier.height(IntrinsicSize.Max)) { Column(Modifier.weight(1f)) { CenteredTextField( scoreA, "0", - Modifier - .focusRequester(requestFocus) - .onFocusChanged { - keyboardController?.hide() - updateFocusStateA(it.isFocused) - } - ) + focusStateA, + requestFocusA + ) { 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)) { CenteredTextField( scoreB, "0", - Modifier - .onFocusChanged { - keyboardController?.hide() - updateFocusStateB(it.isFocused) - } - ) + focusStateB, + requestFocusB + ) { + updateFocusStateB(it.isFocused) + } } } + Row { Column(Modifier.weight(1f)) { KeyboardTextButton("1") { @@ -208,23 +241,63 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: () fun CenteredTextField( value: String, placeholder: String, - modifier: Modifier, + focused: Boolean, + focusRequester: FocusRequester? = null, + onFocusStateChanged: (FocusState) -> Unit ) { - TextField( - value = value, - onValueChange = { }, - placeholder = { - Text( - placeholder, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + + val modifier = if (focusRequester != null) { + Modifier.focusRequester(focusRequester) + } else { + Modifier + } + + Box(contentAlignment = Alignment.Center) { + TextField( + value = value, + onValueChange = { }, + placeholder = { + if (!focused) { + Text( + placeholder, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + }, + textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), + singleLine = true, + readOnly = true, + 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 + ) ) - }, - textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), - singleLine = true, - readOnly = true, - modifier = modifier.fillMaxWidth() - ) + 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) + ) + } + } + } } @@ -235,18 +308,22 @@ fun KeyboardViewPreview() { AppTheme { Surface { KeyboardView( - "", - "350", + "1", + "3511", FocusRequester(), - false, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}) + FocusRequester(), + enableSubmit = false, + focusStateA = true, + focusStateB = false, + updateFocusStateA = {}, + updateFocusStateB = {}, + digitClicked = {}, + addSub100Clicked = {}, + deleteClicked = {}, + negateClicked = {}, + submitClicked = {}, + hideKeyboardClicked = {}, + onSwapClicked = {}) } } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt index d16ca8a..e9d3dfb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/RoundListView.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import me.zobrist.tichucounter.data.Round +import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.ui.AppTheme @Composable @@ -50,7 +50,7 @@ private fun RoundListItem(round: Round, index: Int) { textAlign = TextAlign.Center ) Text( - text = index.toString(), + text = (index + 1).toString(), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), textAlign = TextAlign.Center diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 26bdd03..2dfc42e 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -1,21 +1,27 @@ package me.zobrist.tichucounter.ui.history +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.OpenInFull +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment.Companion.CenterVertically 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.unit.dp import me.zobrist.tichucounter.R -import me.zobrist.tichucounter.data.GameAndScore +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.util.* @@ -23,19 +29,27 @@ import java.util.* @Composable fun HistoryList( viewModel: HistoryViewModel, - showDeleteDialog: Boolean, - onDialogExecuted: (Boolean) -> Unit, navigateToCalculator: () -> Unit ) { - DeleteConfirmDialog(showDeleteDialog, onDialogExecuted) + var showDeleteDialog by remember { mutableStateOf(false) } - HistoryList(viewModel.gameAndHistory, + DeleteConfirmDialog(showDeleteDialog) { + showDeleteDialog = false + if (it) { + viewModel.deleteAllInactiveGames() + } + } + + HistoryList( + viewModel.gameAndHistory, { viewModel.activateGame(it) navigateToCalculator() }, - { viewModel.deleteGame(it) }) + { viewModel.deleteGame(it) }, + { showDeleteDialog = true }, + ) } @Preview @@ -65,22 +79,58 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {} @Composable fun HistoryList( - games: List, + games: List, onOpenClicked: (GameId: Long) -> Unit, - onDeleteClicked: (GameId: Long) -> Unit + onDeleteClicked: (GameId: Long) -> Unit, + onDeleteAllClicked: () -> Unit + ) { Row { LazyColumn { - items(games) { + item { + 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 fun HistoryListItem( - game: GameAndScore, + game: GameWithScores, onOpenClicked: (GameId: Long) -> Unit, onDeleteClicked: (GameId: Long) -> Unit ) { @@ -88,17 +138,20 @@ fun HistoryListItem( DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) - val cardColor = if (game.active) { + val cardColor = if (game.game.active) { CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) } else { CardDefaults.cardColors() } + val totalScores = game.getTotalPoints() + Card( modifier = Modifier .fillMaxWidth() - .padding(all = 4.dp), + .padding(all = 4.dp) + .clickable { onOpenClicked(game.game.uid) }, colors = cardColor ) { Row( @@ -107,34 +160,51 @@ fun HistoryListItem( ) { Column(Modifier.weight(4f)) { Text( - text = game.nameA + " vs " + game.nameB, + text = game.game.nameA + " vs " + game.game.nameB, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.headlineSmall ) Text( - text = game.scoreA.toString() + " : " + game.scoreB.toString(), + text = totalScores.first.toString() + " : " + totalScores.second.toString(), style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.padding(5.dp)) Text( - text = format.format(game.modified), + text = format.format(game.game.modified), style = MaterialTheme.typography.labelSmall ) } Column( Modifier .wrapContentSize() - .width(70.dp) + .width(40.dp) ) { - ElevatedButton(onClick = { onOpenClicked(game.gameId) }, enabled = true) { - Icon(Icons.Outlined.OpenInFull, null) - } - ElevatedButton( - onClick = { onDeleteClicked(game.gameId) }, enabled = !game.active - ) { - Icon(Icons.Outlined.Delete, null) + 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) + } + } + } } } } @@ -145,11 +215,26 @@ fun HistoryListItem( @Composable private fun HistoryListPreview() { val tempData = listOf( - GameAndScore(true, "abc", "def", Date(), Date(), 1, 10, 50), - GameAndScore(false, "ADTH", "dogfg", Date(), Date(), 2, 20, 60), - GameAndScore(false, "TeamA3 langer Name", "TeamB3", Date(), Date(), 3, 30, 70), - GameAndScore(false, "TeamA4", "TeamB4", Date(), Date(), 4, 40, 80), - GameAndScore(false, "TeamA5", "TeamB5", Date(), Date(), 5, 50, 90) + GameWithScores( + Game(true, "abc", "def", Date(), Date()), + listOf(Round(1, 550, 500)) + ), + 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, {}, {}) {} } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt index affda48..ed3f587 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt @@ -7,25 +7,25 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import me.zobrist.tichucounter.data.GameAndScore -import me.zobrist.tichucounter.data.GameDao +import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject @HiltViewModel class HistoryViewModel @Inject constructor( - private val gameDao: GameDao, private val gameRepository: GameRepository ) : ViewModel() { - var gameAndHistory by mutableStateOf(emptyList()) + var gameAndHistory by mutableStateOf(emptyList()) private set init { viewModelScope.launch { - gameDao.getAllWithPoints().collect { games -> - gameAndHistory = games + + gameRepository.getAllWithRoundFlow().collect { games -> + gameAndHistory = + games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed() } } } @@ -42,4 +42,10 @@ class HistoryViewModel @Inject constructor( } } + + fun deleteAllInactiveGames() { + viewModelScope.launch { + gameRepository.deleteAllInactive() + } + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt new file mode 100644 index 0000000..e025a4a --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt @@ -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, + 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 + ) {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt b/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt new file mode 100644 index 0000000..a4dbd23 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/ui/layout/TopBar.kt @@ -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 +) { + 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() + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt index fd86dc5..38840be 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDropDown -import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.End @@ -18,12 +17,15 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.zobrist.tichucounter.R +import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.ui.AppTheme +import me.zobrist.tichucounter.ui.composables.DropDownMenu val languageMap = mapOf( + Language.DEFAULT to R.string.android_default_text, Language.ENGLISH to R.string.english, Language.GERMAN to R.string.german ) @@ -38,7 +40,7 @@ val themeMap = mapOf( @Composable fun SettingsView(viewModel: SettingsViewModel) { SettingsView( - viewModel.screenOn, + viewModel.screenOn.value, viewModel.language, viewModel.theme, { viewModel.updateScreenOn(it) }, @@ -51,7 +53,7 @@ fun SettingsView( valueScreenOn: Boolean = true, valueLanguage: Language = Language.ENGLISH, valueTheme: Theme = Theme.DARK, - updateScreenOn: (Boolean) -> Unit = {}, + updateScreenOn: (KeepScreenOn) -> Unit = {}, updateLanguage: (Language) -> Unit = {}, updateTheme: (Theme) -> Unit = {} ) { @@ -59,7 +61,7 @@ fun SettingsView( BooleanSetting( stringResource(R.string.keep_screen_on), valueScreenOn - ) { updateScreenOn(it) } + ) { updateScreenOn(if (it) KeepScreenOn.ON else KeepScreenOn.OFF) } StringSetting( stringResource(R.string.choose_language_text), @@ -118,7 +120,12 @@ fun StringSetting(name: String, map: Map, selected: T, onSelected: ( .clickable { expanded = true }) { Column(Modifier.weight(5f)) { 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)) { @@ -129,27 +136,18 @@ fun StringSetting(name: String, map: Map, selected: T, onSelected: ( ) } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } + DropDownMenu( + map, + selected, + expanded, ) { - map.forEach { - DropdownMenuItem( - onClick = { - onSelected(it.key) - expanded = false - }, - text = { Text(stringResource(it.value)) }, - trailingIcon = { - if (it.key == selected) { - Icon(Icons.Outlined.Check, contentDescription = null) - } - }) - } + expanded = false + it?.let { onSelected(it) } } } } + @Preview(name = "Light Mode") @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Composable @@ -161,3 +159,20 @@ fun SettingsViewPreview() { } } } + +@Preview(name = "Light Mode") +@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) +@Composable +fun StringSettingPreview() { + + AppTheme { + Surface { + DropDownMenu( + themeMap, + Theme.LIGHT, + true, + ) {} + } + } +} + diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt index 3db0b89..b37e1bd 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.Theme @@ -20,7 +21,7 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte var theme by mutableStateOf(settings.theme) private set - var screenOn by mutableStateOf(false) + var screenOn by mutableStateOf(settings.keepScreenOn) private set fun updateLanguage(language: Language) { @@ -33,7 +34,7 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte this.theme = settings.theme } - fun updateScreenOn(value: Boolean) { + fun updateScreenOn(value: KeepScreenOn) { settings.setKeepScreenOn(value) screenOn = settings.keepScreenOn } diff --git a/app/src/main/res/drawable/app_logo.png b/app/src/main/res/drawable/app_logo.png new file mode 100644 index 0000000..7681534 Binary files /dev/null and b/app/src/main/res/drawable/app_logo.png differ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 05711ba..be60463 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -13,11 +13,14 @@ Ein Aus Neues Spiel - "Erstellt: %s " - Bearbeitet: %s Verlauf löschen Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden. Abbrechen Ok + Löschen + Alle löschen + Aktives Spiel + Vergangene Spiele + Counter \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8b7bbc..e0d8623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,10 +16,14 @@ On Off New Game - Created: %s - Modified: %s Delete history You really want to delete the the history? This action can\'t be undone. Cancel Ok + Delete + Delete all + Current Game + Old Games + Counter + About \ No newline at end of file