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

View File

@@ -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')"
]
}
}

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

View File

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

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 kotlinx.coroutines.flow.Flow
import me.zobrist.tichucounter.data.entity.Game
@Dao
@@ -10,19 +11,13 @@ interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game")
fun getAll(): Flow<List<Game>>
@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<List<GameAndScore>>
@Transaction
@Query("SELECT * FROM game where uid ")
fun getGamesWithRounds(): Flow<List<GameWithScores>>
@Transaction
@Query("SELECT * FROM game WHERE active is 1")
fun getActiveWithRounds(): Flow<GameWithScores?>
@Query("SELECT * FROM game WHERE uid is :gameId")
fun getGameById(gameId: Long): Flow<Game>
@@ -30,6 +25,7 @@ interface GameDao : DaoBase<Game> {
@Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Flow<Game?>
@Query("UPDATE game SET active = 1 WHERE uid is :gameId;")
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
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import me.zobrist.tichucounter.data.entity.Round
@Dao
interface RoundDao : DaoBase<Round> {
@@ -12,20 +12,4 @@ interface RoundDao : DaoBase<Round> {
@Query("SELECT * FROM round WHERE gameId is :gameId")
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
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<ISettingsChangeListener>()
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)
}
}
}

View File

@@ -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)
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.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<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
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)

View File

@@ -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<Round>()
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 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()
}
}
}

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.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() {
}
}

View File

@@ -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<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
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<Round>
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<Round>())
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
}
}

View File

@@ -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 = {})
}
}
}

View File

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

View File

@@ -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<GameAndScore>,
games: List<GameWithScores>,
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, {}, {}) {}
}

View File

@@ -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<GameAndScore>())
var gameAndHistory by mutableStateOf(emptyList<GameWithScores>())
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()
}
}
}

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.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 <T> StringSetting(name: String, map: Map<T, Int>, 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 <T> StringSetting(name: String, map: Map<T, Int>, 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,
) {}
}
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -13,11 +13,14 @@
<string name="on">Ein</string>
<string name="off">Aus</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_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
<string name="ok">Ok</string>
<string name="delete">Löschen</string>
<string name="deleteAll">Alle löschen</string>
<string name="active">Aktives Spiel</string>
<string name="inactive">Vergangene Spiele</string>
<string name="menu_counter">Counter</string>
</resources>

View File

@@ -16,10 +16,14 @@
<string name="on">On</string>
<string name="off">Off</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_text">You really want to delete the the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="delete">Delete</string>
<string name="deleteAll">Delete all</string>
<string name="active">Current Game</string>
<string name="inactive">Old Games</string>
<string name="menu_counter">Counter</string>
<string name="menu_about">About</string>
</resources>