Merge pull request 'release/2.0' (#22) from release/2.0 into develop
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: fabian/TichuCounter#22
This commit was merged in pull request #22.
This commit is contained in:
2023-01-28 23:30:57 +01:00
44 changed files with 1162 additions and 677 deletions

View File

@@ -15,8 +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 = 1 def versionMajor = 2
def versionMinor = 1 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))

View File

@@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "f07e88c78e54c69c73890495a2121bf4", "identityHash": "1739540cd7d5436941316932a1036d83",
"entities": [ "entities": [
{ {
"tableName": "Round", "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": [ "fields": [
{ {
"fieldPath": "gameId", "fieldPath": "gameId",
@@ -30,7 +30,7 @@
"fieldPath": "uid", "fieldPath": "uid",
"columnName": "uid", "columnName": "uid",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false "notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@@ -44,7 +44,7 @@
}, },
{ {
"tableName": "Game", "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": [ "fields": [
{ {
"fieldPath": "active", "fieldPath": "active",
@@ -80,7 +80,7 @@
"fieldPath": "uid", "fieldPath": "uid",
"columnName": "uid", "columnName": "uid",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false "notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@@ -96,7 +96,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

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 != 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,6 +51,8 @@ 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()
@@ -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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MyScaffoldLayout( fun MyScaffoldLayout(
@@ -72,6 +152,8 @@ class MainActivity : BaseActivity() {
fabAction: () -> Unit fabAction: () -> Unit
) { ) {
var topBarState by remember { mutableStateOf(TopBarState()) }
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
if (showFab) { if (showFab) {
@@ -81,199 +163,72 @@ class MainActivity : BaseActivity() {
} }
} }
}, },
topBar = { topBar = { TopBar(topBarState) }) { paddings ->
TopBar(
mainViewModel.topBarTitle,
mainViewModel.topBarIcon,
{ mainViewModel.onNavigateClicked() },
mainViewModel.topBarActions
)
}) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "counter", startDestination = Route.COUNTER.name,
modifier = Modifier.padding(it) modifier = Modifier.padding(paddings)
) { ) {
composable("counter") { composable(Route.COUNTER) {
Counter(counterViewModel)
mainViewModel.topBarActions = (listOf( var expanded by remember { mutableStateOf(false) }
topBarState = TopBarState(
title = stringResource(R.string.app_name),
actions = (listOf(
TopBarAction( TopBarAction(
Icons.Outlined.Undo, Icons.Outlined.Undo,
mainViewModel.isUndoActionActive mainViewModel.isUndoActionActive,
) { mainViewModel.undoLastRound() }, { mainViewModel.undoLastRound() }),
TopBarAction( TopBarAction(
Icons.Outlined.Redo, Icons.Outlined.Redo,
mainViewModel.isRedoActionActive mainViewModel.isRedoActionActive,
) { mainViewModel.redoLastRound() } { 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()
}
}
}
},
)) ))
mainViewModel.topBarIcon = Icons.Outlined.Menu ) { scope.launch { drawerState.open() } }
mainViewModel.topBarTitle = stringResource(R.string.app_name)
mainViewModel.topBarNavigationAction =
NavigationAction { scope.launch { drawerState.open() } }
}
composable("history") {
var openDialog by remember { mutableStateOf(false) } Counter(counterViewModel)
}
composable(Route.HISTORY) {
topBarState =
TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } }
HistoryList(historyViewModel, openDialog, { deleteAll -> HistoryList(historyViewModel) { navController.navigate(Route.COUNTER) }
if (deleteAll) {
mainViewModel.deleteAllInactiveGames()
} }
openDialog = false composable(Route.SETTINGS) {
}) { navController.navigate("counter") } topBarState =
mainViewModel.topBarActions = listOf( TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } }
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) 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(Route.ABOUT) {
@Composable topBarState =
private fun TopBar( TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } }
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,
)
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class) AboutView()
@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
}
// 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)
)
} }
} }
} }
) {
MyScaffoldLayout(
drawerState,
scope,
navController,
counterViewModel.keyboardHidden
) { counterViewModel.keyboardHidden = false }
} }
} }
private class Screen(val route: String, val icon: ImageVector, @StringRes val resourceId: Int)
}

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

@@ -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,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

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,7 +1,7 @@
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> {
@@ -12,20 +12,4 @@ interface RoundDao : DaoBase<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"
)
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
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

@@ -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 action: () -> 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
return try {
var setting = sharedPreferences.getString(Language::class.simpleName, null) var theme: Theme
enumValueOf(setting!!) private set
var keepScreenOn: KeepScreenOn
private set
init {
language = try {
enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!)
} catch (_: NullPointerException) { } catch (_: NullPointerException) {
val current = getCurrentAppLanguage() Language.DEFAULT
setLanguage(current) }
current
theme = try {
enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!)
} catch (_: java.lang.Exception) {
Theme.DEFAULT
}
keepScreenOn = try {
enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!)
} catch (_: java.lang.Exception) {
KeepScreenOn.OFF
} }
} }
val theme: Theme fun registerOnChangeListener(listener: ISettingsChangeListener) {
get() { listenerList.add(listener)
val setting = sharedPreferences.getString(Theme::class.simpleName, Theme.DEFAULT.name)
return enumValueOf(setting!!) listener.onThemeChanged(theme)
listener.onLanguageChanged(language)
listener.onScreenOnChanged(keepScreenOn)
} }
val keepScreenOn: Boolean fun unregisterOnChangeListener(listener: ISettingsChangeListener?) {
get() { if (listener != null) {
return sharedPreferences.getBoolean("keep_screen_on", false) listenerList.remove(listener)
}
private fun getCurrentAppLanguage(): Language {
return when (getApplicationLocales()[0].toString()) {
"de" -> Language.GERMAN
else -> Language.ENGLISH
} }
} }
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.apply() notifyListeners(language)
} }
fun setTheme(theme: Theme) { 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() val editor = sharedPreferences.edit()
editor.putString(Theme::class.simpleName, theme.name) editor.putString(name, value)
editor.apply() editor.apply()
} }
fun setKeepScreenOn(setting: Boolean) { private fun notifyListeners(language: Language) {
val editor = sharedPreferences.edit() listenerList.forEach {
editor.putBoolean("keep_screen_on", setting) it.onLanguageChanged(language)
editor.apply() }
}
private fun notifyListeners(theme: Theme) {
listenerList.forEach {
it.onThemeChanged(theme)
}
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
listenerList.forEach {
it.onScreenOnChanged(keepScreenOn)
}
} }
} }

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

@@ -2,13 +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.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
@@ -81,7 +83,7 @@ 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)
} }
@@ -90,7 +92,7 @@ class GameRepository @Inject constructor(
suspend fun deleteGame(uid: Long) { suspend fun deleteGame(uid: Long) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
gameDao.getGameById(uid).take(1).collect() { gameDao.getGameById(uid).take(1).collect {
gameDao.delete(it) gameDao.delete(it)
val rounds = roundDao.getAllForGame(it.uid) val rounds = roundDao.getAllForGame(it.uid)
roundDao.delete(rounds) roundDao.delete(rounds)
@@ -103,7 +105,7 @@ class GameRepository @Inject constructor(
suspend fun deleteAllInactive() { suspend fun deleteAllInactive() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
gameDao.getAll().take(1).collect() { games -> gameDao.getAll().take(1).collect { games ->
val activeId = games.first { it.active }.uid val activeId = games.first { it.active }.uid
val gamesToDelete = games.filter { !it.active } val gamesToDelete = games.filter { !it.active }
@@ -116,4 +118,12 @@ class GameRepository @Inject constructor(
} }
} }
} }
fun getActiveGameFlow(): Flow<GameWithScores?> {
return gameDao.getActiveWithRounds()
}
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {
return gameDao.getGamesWithRounds()
}
} }

View File

@@ -1,21 +1,21 @@
@file:Suppress("unused", "unused", "unused") @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)
@@ -24,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)
@@ -55,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.action()
} }
fun undoLastRound() { fun undoLastRound() {
@@ -83,10 +78,4 @@ class MainViewModel @Inject constructor(
gameRepository.newGame() gameRepository.newGame()
} }
} }
fun deleteAllInactiveGames() {
viewModelScope.launch {
gameRepository.deleteAllInactive()
}
}
} }

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,21 +49,10 @@ 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() },
{ viewModel.keyboardHidden = true })
} }
} }
} }
@@ -90,19 +79,7 @@ fun Portrait(viewModel: ICounterViewModel) {
) )
if (!viewModel.keyboardHidden) { if (!viewModel.keyboardHidden) {
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() },
{ viewModel.keyboardHidden = true })
} }
} }
} }
@@ -130,11 +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 var keyboardHidden: Boolean = false
override fun giveFocusToAIfNone() { override fun focusLastInput() {
} }
override fun updateOtherScore() { override fun updateOtherScore() {
@@ -174,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,30 +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
var keyboardHidden: Boolean
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()
@@ -40,43 +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) override var keyboardHidden by mutableStateOf(false)
private set
override var activeValue: String override var activeValue: String
get() { get() {
@@ -110,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()
} }
} }
@@ -178,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()
@@ -186,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("-", "")
@@ -198,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
@@ -239,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,34 +1,59 @@
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.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.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.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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,
@@ -36,39 +61,47 @@ fun KeyboardView(
deleteClicked: () -> Unit, deleteClicked: () -> Unit,
negateClicked: () -> Unit, negateClicked: () -> Unit,
submitClicked: () -> Unit, submitClicked: () -> Unit,
hideKeyboardClicked: () -> 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") {
@@ -208,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)
)
}
}
}
} }
@@ -235,18 +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

@@ -17,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
@@ -50,7 +50,7 @@ private fun RoundListItem(round: Round, index: Int) {
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

@@ -1,21 +1,27 @@
package me.zobrist.tichucounter.ui.history package me.zobrist.tichucounter.ui.history
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
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.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteForever
import androidx.compose.material.icons.outlined.OpenInFull import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material3.* 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.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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.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.text.DateFormat
import java.util.* import java.util.*
@@ -23,19 +29,27 @@ import java.util.*
@Composable @Composable
fun HistoryList( fun HistoryList(
viewModel: HistoryViewModel, viewModel: HistoryViewModel,
showDeleteDialog: Boolean,
onDialogExecuted: (Boolean) -> Unit,
navigateToCalculator: () -> 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) viewModel.activateGame(it)
navigateToCalculator() navigateToCalculator()
}, },
{ viewModel.deleteGame(it) }) { viewModel.deleteGame(it) },
{ showDeleteDialog = true },
)
} }
@Preview @Preview
@@ -65,22 +79,58 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {}
@Composable @Composable
fun HistoryList( fun HistoryList(
games: List<GameAndScore>, games: List<GameWithScores>,
onOpenClicked: (GameId: Long) -> Unit, onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit onDeleteClicked: (GameId: Long) -> Unit,
onDeleteAllClicked: () -> Unit
) { ) {
Row { Row {
LazyColumn { 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) 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( fun HistoryListItem(
game: GameAndScore, game: GameWithScores,
onOpenClicked: (GameId: Long) -> Unit, onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit onDeleteClicked: (GameId: Long) -> Unit
) { ) {
@@ -88,17 +138,20 @@ fun HistoryListItem(
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) 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) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
} else { } else {
CardDefaults.cardColors() 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 colors = cardColor
) { ) {
Row( Row(
@@ -107,34 +160,51 @@ fun HistoryListItem(
) { ) {
Column(Modifier.weight(4f)) { Column(Modifier.weight(4f)) {
Text( Text(
text = game.nameA + " vs " + game.nameB, text = game.game.nameA + " vs " + game.game.nameB,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
Text( Text(
text = game.scoreA.toString() + " : " + game.scoreB.toString(), text = totalScores.first.toString() + " : " + totalScores.second.toString(),
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
Spacer(modifier = Modifier.padding(5.dp)) Spacer(modifier = Modifier.padding(5.dp))
Text( Text(
text = format.format(game.modified), text = format.format(game.game.modified),
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
} }
Column( Column(
Modifier Modifier
.wrapContentSize() .wrapContentSize()
.width(70.dp) .width(40.dp)
) { ) {
ElevatedButton(onClick = { onOpenClicked(game.gameId) }, enabled = true) { if (!game.game.active) {
Icon(Icons.Outlined.OpenInFull, null) var expanded by remember { mutableStateOf(false) }
}
ElevatedButton( Icon(
onClick = { onDeleteClicked(game.gameId) }, enabled = !game.active 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,
) { ) {
Icon(Icons.Outlined.Delete, null) expanded = false
it?.let {
when (it) {
"delete" -> onDeleteClicked(game.game.uid)
}
}
}
} }
} }
} }
@@ -145,11 +215,26 @@ fun HistoryListItem(
@Composable @Composable
private fun HistoryListPreview() { private fun HistoryListPreview() {
val tempData = listOf( val tempData = listOf(
GameAndScore(true, "abc", "def", Date(), Date(), 1, 10, 50), GameWithScores(
GameAndScore(false, "ADTH", "dogfg", Date(), Date(), 2, 20, 60), Game(true, "abc", "def", Date(), Date()),
GameAndScore(false, "TeamA3 langer Name", "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,25 +7,25 @@ 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 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 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()
} }
} }
} }
@@ -42,4 +42,10 @@ class HistoryViewModel @Inject constructor(
} }
} }
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

@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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
@@ -18,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
) )
@@ -38,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) },
@@ -51,7 +53,7 @@ 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 = {}
) { ) {
@@ -59,7 +61,7 @@ fun SettingsView(
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),
@@ -118,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)) {
@@ -129,27 +136,18 @@ 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 = "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
@@ -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,
) {}
}
}
}

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
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -13,11 +13,14 @@
<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="created">"Erstellt: %s "</string>
<string name="modified">Bearbeitet: %s</string>
<string name="delete_inactive_title">Verlauf löschen</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="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="cancel">Abbrechen</string>
<string name="ok">Ok</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

@@ -16,10 +16,14 @@
<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="created">Created: %s</string>
<string name="modified">Modified: %s</string>
<string name="delete_inactive_title">Delete history</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="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="cancel">Cancel</string>
<string name="ok">Ok</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>