commit 2f4820028f57535d919603324dc8b1c174a8cb44 Author: Tinsae Date: Thu Dec 25 13:22:26 2025 +0100 A prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ef6e938 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "net.tinsae.clocked" + compileSdk = 36 //{ + // version = release(36) + //} + + defaultConfig { + applicationId = "net.tinsae.clocked" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } + } + buildFeatures { + compose = true + } +} + +dependencies { + coreLibraryDesugaring(libs.android.desugar.jdk.libs) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite) + implementation(libs.androidx.compose.material3.window.size.class1) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/net/tinsae/clocked/ExampleInstrumentedTest.kt b/app/src/androidTest/java/net/tinsae/clocked/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1d08369 --- /dev/null +++ b/app/src/androidTest/java/net/tinsae/clocked/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package net.tinsae.clocked + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("net.tinsae.clocked", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d94418 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/LoginScreen.kt b/app/src/main/java/net/tinsae/clocked/LoginScreen.kt new file mode 100644 index 0000000..bc17478 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/LoginScreen.kt @@ -0,0 +1,86 @@ +package net.tinsae.clocked + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.tinsae.clocked.ui.theme.ClockedTheme + +@Composable +fun LoginScreen(onLoginSuccess: () -> Unit, modifier: Modifier = Modifier) { + var email by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + + Scaffold(modifier = modifier.fillMaxSize()) { innerPadding -> + // 1. Use imePadding() to automatically handle keyboard insets. + // 2. Add verticalScroll to allow scrolling on small screens. + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .imePadding() // Automatically adds padding when the keyboard is open + .verticalScroll(rememberScrollState()) // Allows scrolling if content is too large + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Welcome to Clocked", + style = MaterialTheme.typography.headlineLarge + ) + + Spacer(modifier = Modifier.height(32.dp)) + + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + singleLine = true + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = onLoginSuccess, + modifier = Modifier.fillMaxWidth() + ) { + Text("Log In") + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LoginScreenPreview() { + ClockedTheme { + LoginScreen(onLoginSuccess = {}) + } +} diff --git a/app/src/main/java/net/tinsae/clocked/MainActivity.kt b/app/src/main/java/net/tinsae/clocked/MainActivity.kt new file mode 100644 index 0000000..7096d77 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/MainActivity.kt @@ -0,0 +1,170 @@ +package net.tinsae.clocked + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import net.tinsae.clocked.dashboard.DashboardScreen +import net.tinsae.clocked.history.HistoryScreen +import net.tinsae.clocked.ui.theme.ClockedTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ClockedTheme { + // The root of the app now decides whether to show Login or Main content + AppEntry() + } + } + } +} + +@Composable +fun AppEntry() { + // For now, we'll fake authentication. In a real app, this would come from a ViewModel or repository. + var isAuthenticated by rememberSaveable { mutableStateOf(true) } // Start as not authenticated + + if (isAuthenticated) { + // If authenticated, show the main app UI + ClockedApp() + } else { + // If not, show the Login screen + LoginScreen(onLoginSuccess = { }) + } +} + +@PreviewScreenSizes +@Composable +fun ClockedApp() { + var currentDestination by rememberSaveable { mutableStateOf(AppDestinations.HOME) } + + Scaffold( + modifier = Modifier.fillMaxSize(), // Removed background for clarity + topBar = { + Box( + modifier = Modifier + .fillMaxWidth() + // Apply padding ONLY for the top safe area (status bar) + .padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top).asPaddingValues()) + .background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.CenterStart + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + }, + ) { innerPadding -> // This padding from the parent Scaffold accounts for the TopBar. + //val layoutDirection = LocalLayoutDirection.current + val contentPadding = PaddingValues( + // Respect the horizontal padding for gestures, etc. + //start = innerPadding.calculateStartPadding(layoutDirection), + //end = innerPadding.calculateEndPadding(layoutDirection), + // Respect the top padding to stay below the top bar. + top = innerPadding.calculateTopPadding(), + // CRITICAL: Ignore the bottom padding from the parent Scaffold. + bottom = 0.dp + ) + + NavigationSuiteScaffold( + // Apply our custom-calculated padding. + modifier = Modifier.padding(contentPadding), + navigationSuiteItems = { + AppDestinations.entries.forEach { + item( + icon = { Icon(it.icon, contentDescription = it.label) }, + label = { Text(it.label) }, + selected = it == currentDestination, + onClick = { currentDestination = it } + ) + } + } + ) { + // The router now shows the correct screen based on the destination. + when (currentDestination) { + AppDestinations.HOME -> DashboardScreen(modifier = Modifier.fillMaxSize()) + AppDestinations.HISTORY -> HistoryScreen(modifier = Modifier.fillMaxSize()) + AppDestinations.PROFILE -> Greeting(name = "Profile", modifier = Modifier.fillMaxSize()) + } + } + } +} + + + +enum class AppDestinations( + val label: String, + val icon: ImageVector, +) { + HOME("Home", Icons.Default.Home), + HISTORY("History", Icons.AutoMirrored.Filled.List), + PROFILE("Profile", Icons.Default.AccountBox), +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Scaffold(modifier = modifier) { innerPadding -> + Text( + text = "Hello $name!", + modifier = Modifier.padding(innerPadding) + ) + } +} +/* +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + ClockedTheme { + Greeting("Android") + } +} +*/ + +// --- PREVIEW THE ACTUAL APP ENTRY POINT --- +@Preview(showBackground = true) +@Composable +fun AppEntryPreview() { + ClockedTheme { + // This preview will correctly show the LoginScreen because isAuthenticated starts as false. + AppEntry() + } +} + diff --git a/app/src/main/java/net/tinsae/clocked/components/ActivityList.kt b/app/src/main/java/net/tinsae/clocked/components/ActivityList.kt new file mode 100644 index 0000000..330ac1c --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/components/ActivityList.kt @@ -0,0 +1,205 @@ +package net.tinsae.clocked.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration // Use stable LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.launch +import net.tinsae.clocked.data.Log +import net.tinsae.clocked.service.deleteLog +import net.tinsae.clocked.service.editLog +import net.tinsae.clocked.service.getRecentLogs +import net.tinsae.clocked.ui.theme.cyan +import net.tinsae.clocked.ui.theme.red +import net.tinsae.clocked.util.Util.formatDuration +import net.tinsae.clocked.util.Util.formatTimestampToLocalDateString +import kotlin.math.roundToInt + +@Composable +fun ActivityList( + items: List, + modifier: Modifier = Modifier +) { + var mutableItems by remember { mutableStateOf(items) } + // State to hold the log that should be shown in the dialog. Null means no dialog. + var selectedLogForDialog by remember { mutableStateOf(null) } + + Column(modifier = modifier.fillMaxWidth()) { + Text( + text = "Recent Activity", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + LazyColumn { + items(mutableItems, key = { it.id }) { log -> + ActivityItem( + log = log, + onEdit = { editLog(log) }, + onDelete = { + if (deleteLog(log)) { + mutableItems = mutableItems.filter { it.id != log.id } + } + }, + // When the item is clicked, set it as the selected log for the dialog + onClick = { selectedLogForDialog = log } + ) + } + } + } + + // When a log is selected, show the dialog. + selectedLogForDialog?.let { log -> + DetailsDialog( + log = log, + onDismiss = { selectedLogForDialog = null } + ) + } +} + +@Composable +fun ActivityItem( + log: Log, + onDelete: () -> Unit, + onEdit: () -> Unit, + onClick: () -> Unit, // Add onClick callback + modifier: Modifier = Modifier +) { + val coroutineScope = rememberCoroutineScope() + var offsetX by remember { mutableStateOf(0f) } + val animatedOffsetX by animateFloatAsState(targetValue = offsetX, label = "offset") + + // Use stable LocalConfiguration + val density = LocalDensity.current + val configuration = LocalConfiguration.current + val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() } + val editThreshold = screenWidthPx * 0.4f + val deleteThreshold = -screenWidthPx * 0.4f + + Box( + modifier = modifier + .padding(vertical = 4.dp) + .fillMaxWidth() + ) { + // Background content (icons) + Row( + modifier = Modifier + .matchParentSize() + .clip(MaterialTheme.shapes.medium) + .background(if (animatedOffsetX > 0) cyan else red) + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (animatedOffsetX > 0) { // Swiping right + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit", + tint = Color.White + ) + } + Spacer(modifier = Modifier.weight(1f)) + if (animatedOffsetX < 0) { // Swiping left + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + tint = Color.White + ) + } + } + + // --- Foreground content (the Card) --- + Card( + modifier = Modifier + .offset { IntOffset(animatedOffsetX.roundToInt(), 0) } + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + coroutineScope.launch { + when { + offsetX > editThreshold -> { + onEdit() + offsetX = 0f + } + offsetX < deleteThreshold -> onDelete() + else -> offsetX = 0f + } + } + } + ) { change, dragAmount -> + change.consume() + offsetX += dragAmount + } + } + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + // Use the onClick callback here + .clickable { onClick() }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column { + Row( + modifier = Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = formatTimestampToLocalDateString(log.timestamp), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + Text(text = formatDuration(log.duration), style = MaterialTheme.typography.bodyLarge) + } + Text( + text = log.reason ?: "", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 4.dp) + ) + } + } + } +} + +@Composable +@Preview(showBackground = true) +fun ActivityItemPreview() { + ActivityList(items = getRecentLogs()) +} diff --git a/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt b/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt new file mode 100644 index 0000000..487d472 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/components/DetailsDialog.kt @@ -0,0 +1,68 @@ +package net.tinsae.clocked.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import net.tinsae.clocked.data.Log +import net.tinsae.clocked.util.Util.formatDuration +import net.tinsae.clocked.util.Util.formatTimestampToLocalDateString + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailsDialog(log: Log, onDismiss: () -> Unit) { + ModalBottomSheet( + onDismissRequest = onDismiss, + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text(text = "Log Details", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(12.dp)) + DetailRow("Date:", formatTimestampToLocalDateString(log.timestamp)) + DetailRow("Type:", log.type.toString()) + DetailRow("Duration:", formatDuration(log.duration)) + DetailRow("Reason:", log.reason ?: "No reason provided") + Spacer(modifier = Modifier.height(12.dp)) + Button( + onClick = onDismiss, + modifier = Modifier.align(Alignment.End) + ) { + Text("Close") + } + } + } +} + +@Composable +private fun DetailRow(label: String, value: String) { + // buildAnnotatedString allows mixing different styles in one Text composable. + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(label) + } + append(" ") + append(value) + }, + style = MaterialTheme.typography.bodyLarge // Use a slightly larger font style + ) +} diff --git a/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt new file mode 100644 index 0000000..b5be018 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/dashboard/DashboardScreen.kt @@ -0,0 +1,136 @@ +package net.tinsae.clocked.dashboard + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.tinsae.clocked.components.ActivityList +import net.tinsae.clocked.service.deleteLog +import net.tinsae.clocked.service.editLog +import net.tinsae.clocked.service.getRecentLogs +import net.tinsae.clocked.ui.theme.ClockedTheme +import net.tinsae.clocked.ui.theme.cyan +import net.tinsae.clocked.ui.theme.green +import net.tinsae.clocked.ui.theme.red + +@Composable +fun DashboardScreen(modifier: Modifier = Modifier) { + + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp,0.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + SummaryCard(title = "Overtime", value = "+12h 30m", color = green, modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(16.dp)) + SummaryCard(title = "Time Off", value = "−6h 00m", color = red, modifier = Modifier.weight(1f)) + } + + // ADDED: Manual spacer + Spacer(modifier = Modifier.height(16.dp)) + + NetBalanceCard(value = "+6h 30m") + + // ADDED: Manual spacer + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround + ) { + ActionButton(text = "+ Overtime", modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(16.dp)) + ActionButton(text = "− Time Off", modifier = Modifier.weight(1f)) + } + + // ADDED: Manual spacer + Spacer(modifier = Modifier.height(16.dp)) + + // This section now works as expected + val recentLogs by remember { mutableStateOf(getRecentLogs()) } + ActivityList( + items = recentLogs, + modifier = Modifier.weight(1f) + ) + } +} + + +@Composable +fun SummaryCard(title: String, value: String, color: Color, modifier: Modifier = Modifier) { + Card( + modifier = modifier, + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = title, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = value, style = MaterialTheme.typography.headlineMedium, color = color, fontWeight = FontWeight.Bold) + } + } +} + +@Composable +fun NetBalanceCard(value: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = "Net Balance", style = MaterialTheme. typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = value, style = MaterialTheme.typography.headlineLarge, color = cyan, fontWeight = FontWeight.Bold) + } + } +} + +@Composable +fun ActionButton(text: String, modifier: Modifier = Modifier) { + Button( + onClick = { /*TODO*/ }, + modifier = modifier.fillMaxWidth() ) { + Text(text = text, fontSize = 16.sp, modifier = Modifier.padding(6.dp)) + } +} + +@Preview(showBackground = true) +@Composable +fun DashboardScreenPreview() { + ClockedTheme { + DashboardScreen() + } +} diff --git a/app/src/main/java/net/tinsae/clocked/data/Log.kt b/app/src/main/java/net/tinsae/clocked/data/Log.kt new file mode 100644 index 0000000..4c6e4a7 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/data/Log.kt @@ -0,0 +1,16 @@ +package net.tinsae.clocked.data + +import java.time.Duration +import java.time.Instant + +data class Log( + val id: Int, + val duration: Duration, + val reason: String?, + val type: EntryType, + val timestamp: Instant +) + +enum class EntryType { + OVERTIME, TIME_OFF +} \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt new file mode 100644 index 0000000..5f03c5f --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/history/HistoryScreen.kt @@ -0,0 +1,159 @@ +package net.tinsae.clocked.history + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.tinsae.clocked.components.ActivityItem +import net.tinsae.clocked.components.DetailsDialog +import net.tinsae.clocked.data.Log +import net.tinsae.clocked.service.deleteLog +import net.tinsae.clocked.service.editLog +import net.tinsae.clocked.service.getAllLogs +import net.tinsae.clocked.service.getOvertimeLogs +import net.tinsae.clocked.service.getTimeOffLogs +import net.tinsae.clocked.ui.theme.ClockedTheme +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +// Helper to format the Instant into a readable string like "Tue 18" +fun formatTimestampToDayAndDate(instant: Instant): String { + val formatter = DateTimeFormatter.ofPattern("E d").withZone(ZoneId.systemDefault()) + return formatter.format(instant) +} + +// Helper to format the Instant into a month and year like "March 2025" +fun formatTimestampToMonthYear(instant: Instant): String { + val formatter = DateTimeFormatter.ofPattern("MMMM yyyy").withZone(ZoneId.systemDefault()) + return formatter.format(instant) +} + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HistoryScreen(modifier: Modifier = Modifier) { + var allEntries by remember { mutableStateOf(getAllLogs()) } + val overTimes by remember { mutableStateOf( getOvertimeLogs())} + val timeOffs by remember { mutableStateOf( getTimeOffLogs())} + var selectedLogForDialog by remember { mutableStateOf(null) } + + + val tabs = listOf("All", "Overtime", "Time Off") + var selectedTabIndex by remember { mutableIntStateOf(0) } + + // Filter the list based on the selected tab + // I want this done in service. so I have functions for them + val filteredEntries = remember(selectedTabIndex) { + when (tabs[selectedTabIndex]) { + "Overtime" -> overTimes + "Time Off" -> timeOffs + else -> allEntries + } + } + + // Group the filtered entries by month and year + val groupedEntries = remember(filteredEntries) { + // SIMPLIFIED: No need to destructure a Pair + filteredEntries.groupBy { log -> + formatTimestampToMonthYear(log.timestamp) + } + } + + +Column( + modifier = modifier + .fillMaxSize() + .fillMaxSize() + ) { + PrimaryTabRow (selectedTabIndex = selectedTabIndex) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { Text(title) } + ) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + //.padding(horizontal = 16.dp) + ) { + // Iterate through the grouped map + groupedEntries.forEach { (monthYear, entries) -> + // Add a sticky header for the month + stickyHeader { + MonthHeader(text = monthYear) + } + + // Add the items for that month + items(entries, key = { it.id }) { entry -> + ActivityItem(log = entry, + modifier = Modifier.padding(horizontal = 16.dp), + onDelete = { deleteLog(entry) }, + onEdit = { editLog(entry) }, + onClick = { selectedLogForDialog = entry } + ) + } + } + } + } + + // When a log is selected, show the dialog. + selectedLogForDialog?.let { log -> + DetailsDialog( + log = log, + onDismiss = { selectedLogForDialog = null } + ) + } +} + +@Composable +fun MonthHeader(text: String, modifier: Modifier = Modifier) { + Surface( + modifier = modifier + .fillMaxWidth() + // Use the theme's background color to overlay content correctly + .background(MaterialTheme.colorScheme.background) + ) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp) + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun HistoryScreenPreview() { + ClockedTheme { + HistoryScreen() + } +} diff --git a/app/src/main/java/net/tinsae/clocked/service/LogService.kt b/app/src/main/java/net/tinsae/clocked/service/LogService.kt new file mode 100644 index 0000000..909aa18 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/service/LogService.kt @@ -0,0 +1,82 @@ +package net.tinsae.clocked.service + +import net.tinsae.clocked.data.EntryType +import net.tinsae.clocked.data.Log +import android.util.Log as LOG +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import kotlin.random.Random + +// --- Data for programmatic generation --- +private val overtimeReasons = listOf("Project deadline", "Production hotfix", "Client demo preparation", "Server maintenance", "Completed quarterly reports", "Feature implementation") +private val timeOffReasons = listOf("Left early", "Doctor's appointment", "Personal errand", "Family matter", "Vacation day - Completed quarterly reports") + +// UPDATED: Values are now in ISO 8601 format, matching Supabase's INTERVAL output +private val overtimeValues = listOf("PT1H", "PT2H30M", "PT45M", "PT3H15M", "PT1H45M") +private val timeOffValues = listOf("-PT8H", "-PT4H", "-PT1H30M", "-PT2H") + +// --- Programmatically generate a list of 30 random logs --- +private fun generateRandomLogs(): List { + val logs = mutableListOf() + for (i in 1..30) { + val randomDaysAgo = Random.nextLong(0, 365) + val timestamp = Instant.now().minus(randomDaysAgo, ChronoUnit.DAYS) + val isOvertime = Random.nextBoolean() + + val log = if (isOvertime) { + Log( + id = i, + // Use Duration.parse() to convert the string to a Duration object + duration = Duration.parse(overtimeValues.random()), + reason = if (Random.nextBoolean()) overtimeReasons.random() else null, + type = EntryType.OVERTIME, + timestamp = timestamp + ) + } else { + Log( + id = i, + // Use Duration.parse() here as well + duration = Duration.parse(timeOffValues.random()), + reason = if (Random.nextBoolean()) timeOffReasons.random() else null, + type = EntryType.TIME_OFF, + timestamp = timestamp + ) + } + logs.add(log) + } + return logs +} + + +val logs: List = generateRandomLogs().sortedByDescending { it.timestamp } + +fun getAllLogs(): List { + return logs +} + +fun getOvertimeLogs(): List { + return logs.filter { it.type == EntryType.OVERTIME } +} + +fun getTimeOffLogs(): List { + return logs.filter { it.type == EntryType.TIME_OFF } +} + +fun getRecentLogs(): List { + // This function can now be simplified as the main list is already sorted + // We'll just take the most recent 7 items. + return logs.take(7) +} + + +fun deleteLog(log: Log): Boolean { + logs.filter { it.id != log.id } + LOG.d("TAG","Deleted log with ID: ${log.id}") + return logs.contains(log) +} + +fun editLog(log: Log): Boolean{ + LOG.d("TAG","Edited log with ID: ${log.id}") + return logs.contains(log) +} diff --git a/app/src/main/java/net/tinsae/clocked/ui/theme/Color.kt b/app/src/main/java/net/tinsae/clocked/ui/theme/Color.kt new file mode 100644 index 0000000..a4e2705 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package net.tinsae.clocked.ui.theme + +import androidx.compose.ui.graphics.Color + +val red = Color(0xFFFF5722) +val orange = Color(0xFFFF9800) +val cyan = Color(0xFF4597BE) + +val green = Color(0xFF4CAF50) +val yellow = Color(0xFFFFC107) +val blue = Color(0xFF3F51B5) \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/ui/theme/Theme.kt b/app/src/main/java/net/tinsae/clocked/ui/theme/Theme.kt new file mode 100644 index 0000000..dbc0fe9 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/ui/theme/Theme.kt @@ -0,0 +1,57 @@ +package net.tinsae.clocked.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = red, + secondary = orange, + tertiary = cyan +) + +private val LightColorScheme = lightColorScheme( + primary = green, + secondary = yellow, + tertiary = blue + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ClockedTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/ui/theme/Type.kt b/app/src/main/java/net/tinsae/clocked/ui/theme/Type.kt new file mode 100644 index 0000000..9fb587a --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package net.tinsae.clocked.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/net/tinsae/clocked/util/Util.kt b/app/src/main/java/net/tinsae/clocked/util/Util.kt new file mode 100644 index 0000000..7218909 --- /dev/null +++ b/app/src/main/java/net/tinsae/clocked/util/Util.kt @@ -0,0 +1,48 @@ +package net.tinsae.clocked.util + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.time.Duration + +object Util { + + + fun formatTimestampToLocalDateString(instant: Instant): String { + val formatter = DateTimeFormatter + .ofLocalizedDate(FormatStyle.MEDIUM) // Creates a locale-aware date format + .withZone(ZoneId.systemDefault()) // Converts the UTC Instant to the user's local time zone + return formatter.format(instant) + } + + + fun formatDuration(duration: Duration): String { + if (duration.isZero) { + return "0m" + } + + val totalMinutes = duration.toMinutes() + val sign = if (totalMinutes < 0) "−" else "+" + val absMinutes = kotlin.math.abs(totalMinutes) + val hours = absMinutes / 60 + val minutes = absMinutes % 60 + + return buildString { + append(sign) + if (hours > 0) { + append("${hours}h") + } + if (minutes > 0) { + if (hours > 0) append(" ") + append("${minutes}m") + } + } + } + + + fun formatTimestampToMonthYear(instant: Instant): String { + val formatter = DateTimeFormatter.ofPattern("MMMM yyyy").withZone(ZoneId.systemDefault()) + return formatter.format(instant) + } +} \ No newline at end of file diff --git a/app/src/main/res/README.md b/app/src/main/res/README.md new file mode 100644 index 0000000..85f8c86 --- /dev/null +++ b/app/src/main/res/README.md @@ -0,0 +1,114 @@ +# UI frames. + +#### Main /Dash board +``` +┌──────────────────────────────┐ +│ Clocked │ +│ │ +│ ┌─────────────┐ ┌──────────┐ │ +│ │ Overtime │ │ Time Off │ │ +│ │ +12h 30m │ │ −6h 00m │ │ +│ └─────────────┘ └──────────┘ │ +│ │ +│ ┌──────────────────────────┐ │ +│ │ Net Balance │ │ +│ │ +6h 30m │ │ +│ └──────────────────────────┘ │ +│ │ +│ ┌────────────┐ ┌───────────┐ │ +│ │ + Overtime │ │ − Time │ │ +│ │ │ │ Off │ │ +│ └────────────┘ └───────────┘ │ +│ │ +│ Recent Activity │ +│ +2h 00m • Tue │ +│ −1h 30m • Mon │ +│ +45m • Fri │ +│ See all → │ +└──────────────────────────────┘ +``` +#### Entry form + +``` +┌──────────────────────────────┐ +│ Add Entry │ +│ │ +│ Type │ +│ [ Overtime | Time Off ] │ +│ │ +│ Date │ +│ [ Tue, Mar 18 ▼ ] │ +│ │ +│ Duration │ +│ [ 02 ] h [ 30 ] m │ +│ │ +│ Reason (optional) │ +│ [ Project deadline ] │ +│ │ +│ │ +│ [Save Entry] │ +└──────────────────────────────┘ +``` + +#### History of entries + +``` +┌──────────────────────────────┐ +│ History │ +│ [ All | Overtime | Time Off ]│ +│ │ +│ March 2025 │ +│ +2h 00m Tue 18 │ +│ Project deadline │ +│ │ +│ −1h 30m Mon 16 │ +│ Left early │ +│ │ +│ February 2025 │ +│ +3h 15m Fri 28 │ +│ │ +└──────────────────────────────┘ +``` + +#### Entry detail + +``` +┌──────────────────────────────┐ +│ Entry Details │ +│ │ +│ Type: Overtime │ +│ Date: Tue, Mar 18 │ +│ Duration: 2h 00m │ +│ Reason: Project deadline│ +│ │ +│ [ Edit ] [ Delete ] │ +└──────────────────────────────┘ + +``` + +#### Settings + +``` +┌──────────────────────────────┐ +│ Settings │ +│ │ +│ Data │ +│ ────────────────────────── │ +│ Export to CSV → │ +│ │ +│ Preferences │ +│ ────────────────────────── │ +│ Default duration 8h │ +│ Time rounding 15 min │ +│ Week starts on Monday │ +│ │ +│ Appearance │ +│ ────────────────────────── │ +│ Theme System │ +│ │ +│ About │ +│ ────────────────────────── │ +│ App version 1.0.0 │ +└──────────────────────────────┘ + +``` \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..59d677f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Clocked + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..61e0319 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +