fix gitignore
@@ -13,8 +13,6 @@ npm-debug.*
|
||||
|
||||
# Expo / React Native
|
||||
.expo/
|
||||
app/
|
||||
ios/
|
||||
web-build/
|
||||
|
||||
# Shadow-cljs / Clojure
|
||||
@@ -47,6 +45,8 @@ test/cljd-out/
|
||||
local.properties
|
||||
gradle/wrapper/gradle-wrapper.jar
|
||||
captures/
|
||||
*.keystore
|
||||
*.jks
|
||||
|
||||
# Kotlin
|
||||
*.class
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||
*/
|
||||
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'com.anonymous.todoexpojs'
|
||||
defaultConfig {
|
||||
applicationId 'com.anonymous.todoexpojs'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
||||
shrinkResources enableShrinkResources.toBoolean()
|
||||
minifyEnabled enableMinifyInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
||||
crunchPngs enablePngCrunchInRelease.toBoolean()
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
||||
useLegacyPackaging enableLegacyPackaging.toBoolean()
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# react-native-reanimated
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,25 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.anonymous.todoexpojs
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.anonymous.todoexpojs
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.common.ReleaseLevel
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ExpoReactHostFactory
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactHost: ReactHost by lazy {
|
||||
ExpoReactHostFactory.getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
DefaultNewArchitectureEntryPoint.releaseLevel = try {
|
||||
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ReleaseLevel.STABLE
|
||||
}
|
||||
loadReactNative(this)
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,6 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
||||
<resources/>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#FFFFFF</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Todo App</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.anonymous.todo_flutter"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.anonymous.todo_flutter"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="todo_flutter"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.anonymous.todo_flutter
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.todojava"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.todojava"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.material)
|
||||
implementation(libs.room.runtime)
|
||||
annotationProcessor(libs.room.compiler)
|
||||
implementation(libs.lifecycle.viewmodel)
|
||||
implementation(libs.lifecycle.livedata)
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.constraintlayout)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TodoJava">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.example.todojava;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.example.todojava.data.Todo;
|
||||
import com.example.todojava.databinding.ActivityMainBinding;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityMainBinding binding;
|
||||
private TodoViewModel viewModel;
|
||||
private TodoAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.toolbar);
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(TodoViewModel.class);
|
||||
|
||||
setupRecyclerView();
|
||||
setupFilterChips();
|
||||
setupFab();
|
||||
observeData();
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new TodoAdapter();
|
||||
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.recyclerView.setAdapter(adapter);
|
||||
|
||||
adapter.setOnTodoClickListener(todo -> {
|
||||
TodoFormDialogFragment dialog = TodoFormDialogFragment.newInstance(todo);
|
||||
dialog.show(getSupportFragmentManager(), "edit_todo");
|
||||
});
|
||||
|
||||
adapter.setOnTodoCheckedListener(todo -> viewModel.toggleCompleted(todo));
|
||||
|
||||
// Swipe to delete
|
||||
SwipeToDeleteCallback swipeCallback = new SwipeToDeleteCallback(position -> {
|
||||
Todo todo = adapter.getCurrentList().get(position);
|
||||
viewModel.delete(todo);
|
||||
Snackbar.make(binding.getRoot(), R.string.todo_deleted, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo, v -> viewModel.insert(todo))
|
||||
.show();
|
||||
});
|
||||
new ItemTouchHelper(swipeCallback).attachToRecyclerView(binding.recyclerView);
|
||||
}
|
||||
|
||||
private void setupFilterChips() {
|
||||
binding.chipAll.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.ALL));
|
||||
binding.chipActive.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.ACTIVE));
|
||||
binding.chipDone.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.DONE));
|
||||
}
|
||||
|
||||
private void setupFab() {
|
||||
binding.fab.setOnClickListener(v -> {
|
||||
TodoFormDialogFragment dialog = TodoFormDialogFragment.newInstance();
|
||||
dialog.show(getSupportFragmentManager(), "add_todo");
|
||||
});
|
||||
}
|
||||
|
||||
private void observeData() {
|
||||
viewModel.getFilteredTodos().observe(this, todos -> {
|
||||
adapter.submitList(todos);
|
||||
updateEmptyState(todos);
|
||||
});
|
||||
|
||||
// Update chip text with counts
|
||||
viewModel.getAllCount().observe(this, count -> {
|
||||
String text = getString(R.string.filter_all) + " (" + count + ")";
|
||||
binding.chipAll.setText(text);
|
||||
});
|
||||
|
||||
viewModel.getActiveCount().observe(this, count -> {
|
||||
String text = getString(R.string.filter_active) + " (" + count + ")";
|
||||
binding.chipActive.setText(text);
|
||||
});
|
||||
|
||||
viewModel.getCompletedCount().observe(this, count -> {
|
||||
String text = getString(R.string.filter_done) + " (" + count + ")";
|
||||
binding.chipDone.setText(text);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateEmptyState(List<Todo> todos) {
|
||||
boolean isEmpty = todos == null || todos.isEmpty();
|
||||
binding.emptyState.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
|
||||
binding.recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
|
||||
|
||||
if (isEmpty) {
|
||||
TodoViewModel.Filter filter = viewModel.getCurrentFilter().getValue();
|
||||
if (filter == null) filter = TodoViewModel.Filter.ALL;
|
||||
|
||||
View emptyRoot = binding.emptyState.getRoot();
|
||||
android.widget.TextView titleView = emptyRoot.findViewById(R.id.emptyTitle);
|
||||
android.widget.TextView subtitleView = emptyRoot.findViewById(R.id.emptySubtitle);
|
||||
|
||||
switch (filter) {
|
||||
case ACTIVE:
|
||||
titleView.setText(R.string.empty_active_title);
|
||||
subtitleView.setText(R.string.empty_active_subtitle);
|
||||
break;
|
||||
case DONE:
|
||||
titleView.setText(R.string.empty_done_title);
|
||||
subtitleView.setText(R.string.empty_done_subtitle);
|
||||
break;
|
||||
default:
|
||||
titleView.setText(R.string.empty_state_title);
|
||||
subtitleView.setText(R.string.empty_state_subtitle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.todojava;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
public interface OnSwipeListener {
|
||||
void onSwiped(int position);
|
||||
}
|
||||
|
||||
private final OnSwipeListener listener;
|
||||
private final ColorDrawable background;
|
||||
private final Paint textPaint;
|
||||
|
||||
public SwipeToDeleteCallback(OnSwipeListener listener) {
|
||||
super(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
this.listener = listener;
|
||||
this.background = new ColorDrawable(Color.parseColor("#E53935"));
|
||||
this.textPaint = new Paint();
|
||||
this.textPaint.setColor(Color.WHITE);
|
||||
this.textPaint.setTextSize(40f);
|
||||
this.textPaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
listener.onSwiped(viewHolder.getAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c,
|
||||
@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
|
||||
View itemView = viewHolder.itemView;
|
||||
|
||||
if (dX > 0) {
|
||||
// Swiping right
|
||||
background.setBounds(itemView.getLeft(), itemView.getTop(),
|
||||
itemView.getLeft() + (int) dX, itemView.getBottom());
|
||||
} else if (dX < 0) {
|
||||
// Swiping left
|
||||
background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(),
|
||||
itemView.getRight(), itemView.getBottom());
|
||||
} else {
|
||||
background.setBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
background.draw(c);
|
||||
|
||||
// Draw "Delete" text
|
||||
String deleteText = "Delete";
|
||||
float textWidth = textPaint.measureText(deleteText);
|
||||
float textY = itemView.getTop() + (itemView.getHeight() + textPaint.getTextSize()) / 2f - 4f;
|
||||
|
||||
if (dX > 0) {
|
||||
float textX = itemView.getLeft() + 32f;
|
||||
c.drawText(deleteText, textX, textY, textPaint);
|
||||
} else if (dX < 0) {
|
||||
float textX = itemView.getRight() - textWidth - 32f;
|
||||
c.drawText(deleteText, textX, textY, textPaint);
|
||||
}
|
||||
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.example.todojava;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.todojava.data.Todo;
|
||||
import com.example.todojava.databinding.ItemTodoBinding;
|
||||
|
||||
public class TodoAdapter extends ListAdapter<Todo, TodoAdapter.TodoViewHolder> {
|
||||
|
||||
public interface OnTodoClickListener {
|
||||
void onTodoClick(Todo todo);
|
||||
}
|
||||
|
||||
public interface OnTodoCheckedListener {
|
||||
void onTodoChecked(Todo todo);
|
||||
}
|
||||
|
||||
private OnTodoClickListener clickListener;
|
||||
private OnTodoCheckedListener checkedListener;
|
||||
|
||||
public TodoAdapter() {
|
||||
super(DIFF_CALLBACK);
|
||||
}
|
||||
|
||||
public void setOnTodoClickListener(OnTodoClickListener listener) {
|
||||
this.clickListener = listener;
|
||||
}
|
||||
|
||||
public void setOnTodoCheckedListener(OnTodoCheckedListener listener) {
|
||||
this.checkedListener = listener;
|
||||
}
|
||||
|
||||
private static final DiffUtil.ItemCallback<Todo> DIFF_CALLBACK =
|
||||
new DiffUtil.ItemCallback<Todo>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
|
||||
return oldItem.getId() == newItem.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
|
||||
return oldItem.getTitle().equals(newItem.getTitle())
|
||||
&& oldItem.isCompleted() == newItem.isCompleted()
|
||||
&& oldItem.getCategory().equals(newItem.getCategory())
|
||||
&& oldItem.getPriority().equals(newItem.getPriority());
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemTodoBinding binding = ItemTodoBinding.inflate(
|
||||
LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new TodoViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
|
||||
Todo todo = getItem(position);
|
||||
holder.bind(todo);
|
||||
}
|
||||
|
||||
class TodoViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ItemTodoBinding binding;
|
||||
|
||||
TodoViewHolder(ItemTodoBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
void bind(Todo todo) {
|
||||
binding.textTitle.setText(todo.getTitle());
|
||||
binding.chipCategory.setText(todo.getCategory());
|
||||
|
||||
// Handle completed state
|
||||
binding.checkBox.setOnCheckedChangeListener(null);
|
||||
binding.checkBox.setChecked(todo.isCompleted());
|
||||
|
||||
if (todo.isCompleted()) {
|
||||
binding.textTitle.setPaintFlags(
|
||||
binding.textTitle.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
binding.textTitle.setTextColor(
|
||||
ContextCompat.getColor(itemView.getContext(), R.color.completed_text));
|
||||
} else {
|
||||
binding.textTitle.setPaintFlags(
|
||||
binding.textTitle.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
binding.textTitle.setTextColor(
|
||||
ContextCompat.getColor(itemView.getContext(), R.color.on_surface));
|
||||
}
|
||||
|
||||
// Priority dot color
|
||||
int priorityColor;
|
||||
switch (todo.getPriority()) {
|
||||
case "High":
|
||||
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_high);
|
||||
break;
|
||||
case "Medium":
|
||||
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_medium);
|
||||
break;
|
||||
default:
|
||||
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_low);
|
||||
break;
|
||||
}
|
||||
GradientDrawable dot = (GradientDrawable) binding.priorityDot.getBackground().mutate();
|
||||
dot.setColor(priorityColor);
|
||||
|
||||
// Click listener for editing
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (clickListener != null) {
|
||||
clickListener.onTodoClick(todo);
|
||||
}
|
||||
});
|
||||
|
||||
// Checkbox listener
|
||||
binding.checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (checkedListener != null) {
|
||||
checkedListener.onTodoChecked(todo);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.example.todojava;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.example.todojava.data.Todo;
|
||||
import com.example.todojava.databinding.DialogTodoFormBinding;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class TodoFormDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String ARG_TODO_ID = "todo_id";
|
||||
private static final String ARG_TODO_TITLE = "todo_title";
|
||||
private static final String ARG_TODO_CATEGORY = "todo_category";
|
||||
private static final String ARG_TODO_PRIORITY = "todo_priority";
|
||||
private static final String ARG_TODO_COMPLETED = "todo_completed";
|
||||
private static final String ARG_TODO_CREATED_AT = "todo_created_at";
|
||||
|
||||
private DialogTodoFormBinding binding;
|
||||
private TodoViewModel viewModel;
|
||||
|
||||
public static TodoFormDialogFragment newInstance() {
|
||||
return new TodoFormDialogFragment();
|
||||
}
|
||||
|
||||
public static TodoFormDialogFragment newInstance(Todo todo) {
|
||||
TodoFormDialogFragment fragment = new TodoFormDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(ARG_TODO_ID, todo.getId());
|
||||
args.putString(ARG_TODO_TITLE, todo.getTitle());
|
||||
args.putString(ARG_TODO_CATEGORY, todo.getCategory());
|
||||
args.putString(ARG_TODO_PRIORITY, todo.getPriority());
|
||||
args.putBoolean(ARG_TODO_COMPLETED, todo.isCompleted());
|
||||
args.putLong(ARG_TODO_CREATED_AT, todo.getCreatedAt());
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private boolean isEditing() {
|
||||
return getArguments() != null && getArguments().containsKey(ARG_TODO_ID);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
binding = DialogTodoFormBinding.inflate(LayoutInflater.from(getContext()));
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(TodoViewModel.class);
|
||||
|
||||
if (isEditing()) {
|
||||
prefillForm();
|
||||
}
|
||||
|
||||
String title = isEditing() ? getString(R.string.edit_todo) : getString(R.string.add_todo);
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
.setView(binding.getRoot())
|
||||
.setPositiveButton(R.string.save, (dialog, which) -> saveTodo())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
private void prefillForm() {
|
||||
Bundle args = getArguments();
|
||||
if (args == null) return;
|
||||
|
||||
binding.editTitle.setText(args.getString(ARG_TODO_TITLE, ""));
|
||||
|
||||
String category = args.getString(ARG_TODO_CATEGORY, "Personal");
|
||||
switch (category) {
|
||||
case "Personal":
|
||||
binding.chipPersonal.setChecked(true);
|
||||
break;
|
||||
case "Work":
|
||||
binding.chipWork.setChecked(true);
|
||||
break;
|
||||
case "Shopping":
|
||||
binding.chipShopping.setChecked(true);
|
||||
break;
|
||||
case "Health":
|
||||
binding.chipHealth.setChecked(true);
|
||||
break;
|
||||
case "Learning":
|
||||
binding.chipLearning.setChecked(true);
|
||||
break;
|
||||
}
|
||||
|
||||
String priority = args.getString(ARG_TODO_PRIORITY, "Medium");
|
||||
switch (priority) {
|
||||
case "Low":
|
||||
binding.chipLow.setChecked(true);
|
||||
break;
|
||||
case "Medium":
|
||||
binding.chipMedium.setChecked(true);
|
||||
break;
|
||||
case "High":
|
||||
binding.chipHigh.setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String getSelectedCategory() {
|
||||
int checkedId = binding.categoryChipGroup.getCheckedChipId();
|
||||
if (checkedId == R.id.chipPersonal) return "Personal";
|
||||
if (checkedId == R.id.chipWork) return "Work";
|
||||
if (checkedId == R.id.chipShopping) return "Shopping";
|
||||
if (checkedId == R.id.chipHealth) return "Health";
|
||||
if (checkedId == R.id.chipLearning) return "Learning";
|
||||
return "Personal";
|
||||
}
|
||||
|
||||
private String getSelectedPriority() {
|
||||
int checkedId = binding.priorityChipGroup.getCheckedChipId();
|
||||
if (checkedId == R.id.chipLow) return "Low";
|
||||
if (checkedId == R.id.chipMedium) return "Medium";
|
||||
if (checkedId == R.id.chipHigh) return "High";
|
||||
return "Medium";
|
||||
}
|
||||
|
||||
private void saveTodo() {
|
||||
String title = binding.editTitle.getText() != null
|
||||
? binding.editTitle.getText().toString().trim() : "";
|
||||
|
||||
if (title.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String category = getSelectedCategory();
|
||||
String priority = getSelectedPriority();
|
||||
|
||||
if (isEditing()) {
|
||||
Bundle args = getArguments();
|
||||
Todo todo = new Todo(title, category, priority);
|
||||
todo.setId(args.getLong(ARG_TODO_ID));
|
||||
todo.setCompleted(args.getBoolean(ARG_TODO_COMPLETED, false));
|
||||
todo.setCreatedAt(args.getLong(ARG_TODO_CREATED_AT, System.currentTimeMillis()));
|
||||
viewModel.update(todo);
|
||||
} else {
|
||||
Todo todo = new Todo(title, category, priority);
|
||||
viewModel.insert(todo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.example.todojava;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.example.todojava.data.Todo;
|
||||
import com.example.todojava.data.TodoDao;
|
||||
import com.example.todojava.data.TodoDatabase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class TodoViewModel extends AndroidViewModel {
|
||||
|
||||
public enum Filter {
|
||||
ALL, ACTIVE, DONE
|
||||
}
|
||||
|
||||
private final TodoDao todoDao;
|
||||
private final ExecutorService executor;
|
||||
|
||||
private final LiveData<List<Todo>> allTodos;
|
||||
private final LiveData<List<Todo>> activeTodos;
|
||||
private final LiveData<List<Todo>> completedTodos;
|
||||
|
||||
private final LiveData<Integer> allCount;
|
||||
private final LiveData<Integer> activeCount;
|
||||
private final LiveData<Integer> completedCount;
|
||||
|
||||
private final MutableLiveData<Filter> currentFilter = new MutableLiveData<>(Filter.ALL);
|
||||
private final MediatorLiveData<List<Todo>> filteredTodos = new MediatorLiveData<>();
|
||||
|
||||
public TodoViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
TodoDatabase db = TodoDatabase.getInstance(application);
|
||||
todoDao = db.todoDao();
|
||||
executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
allTodos = todoDao.getAllTodos();
|
||||
activeTodos = todoDao.getActiveTodos();
|
||||
completedTodos = todoDao.getCompletedTodos();
|
||||
|
||||
allCount = todoDao.getAllCount();
|
||||
activeCount = todoDao.getActiveCount();
|
||||
completedCount = todoDao.getCompletedCount();
|
||||
|
||||
filteredTodos.addSource(allTodos, todos -> {
|
||||
if (currentFilter.getValue() == Filter.ALL) {
|
||||
filteredTodos.setValue(todos);
|
||||
}
|
||||
});
|
||||
filteredTodos.addSource(activeTodos, todos -> {
|
||||
if (currentFilter.getValue() == Filter.ACTIVE) {
|
||||
filteredTodos.setValue(todos);
|
||||
}
|
||||
});
|
||||
filteredTodos.addSource(completedTodos, todos -> {
|
||||
if (currentFilter.getValue() == Filter.DONE) {
|
||||
filteredTodos.setValue(todos);
|
||||
}
|
||||
});
|
||||
filteredTodos.addSource(currentFilter, filter -> {
|
||||
switch (filter) {
|
||||
case ALL:
|
||||
filteredTodos.setValue(allTodos.getValue());
|
||||
break;
|
||||
case ACTIVE:
|
||||
filteredTodos.setValue(activeTodos.getValue());
|
||||
break;
|
||||
case DONE:
|
||||
filteredTodos.setValue(completedTodos.getValue());
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<List<Todo>> getFilteredTodos() {
|
||||
return filteredTodos;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getAllCount() {
|
||||
return allCount;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getActiveCount() {
|
||||
return activeCount;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getCompletedCount() {
|
||||
return completedCount;
|
||||
}
|
||||
|
||||
public MutableLiveData<Filter> getCurrentFilter() {
|
||||
return currentFilter;
|
||||
}
|
||||
|
||||
public void setFilter(Filter filter) {
|
||||
currentFilter.setValue(filter);
|
||||
}
|
||||
|
||||
public void insert(Todo todo) {
|
||||
executor.execute(() -> todoDao.insert(todo));
|
||||
}
|
||||
|
||||
public void update(Todo todo) {
|
||||
executor.execute(() -> todoDao.update(todo));
|
||||
}
|
||||
|
||||
public void delete(Todo todo) {
|
||||
executor.execute(() -> todoDao.delete(todo));
|
||||
}
|
||||
|
||||
public void toggleCompleted(Todo todo) {
|
||||
todo.setCompleted(!todo.isCompleted());
|
||||
executor.execute(() -> todoDao.update(todo));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.example.todojava.data;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity(tableName = "todos")
|
||||
public class Todo {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public long id;
|
||||
|
||||
@ColumnInfo(name = "title")
|
||||
public String title;
|
||||
|
||||
@ColumnInfo(name = "completed")
|
||||
public boolean completed;
|
||||
|
||||
@ColumnInfo(name = "category")
|
||||
public String category;
|
||||
|
||||
@ColumnInfo(name = "priority")
|
||||
public String priority;
|
||||
|
||||
@ColumnInfo(name = "created_at")
|
||||
public long createdAt;
|
||||
|
||||
public Todo() {
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public Todo(String title, String category, String priority) {
|
||||
this.title = title;
|
||||
this.completed = false;
|
||||
this.category = category;
|
||||
this.priority = priority;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return completed;
|
||||
}
|
||||
|
||||
public void setCompleted(boolean completed) {
|
||||
this.completed = completed;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public String getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(String priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.todojava.data;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface TodoDao {
|
||||
|
||||
@Query("SELECT * FROM todos ORDER BY created_at DESC")
|
||||
LiveData<List<Todo>> getAllTodos();
|
||||
|
||||
@Query("SELECT * FROM todos WHERE completed = 0 ORDER BY created_at DESC")
|
||||
LiveData<List<Todo>> getActiveTodos();
|
||||
|
||||
@Query("SELECT * FROM todos WHERE completed = 1 ORDER BY created_at DESC")
|
||||
LiveData<List<Todo>> getCompletedTodos();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM todos")
|
||||
LiveData<Integer> getAllCount();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM todos WHERE completed = 0")
|
||||
LiveData<Integer> getActiveCount();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM todos WHERE completed = 1")
|
||||
LiveData<Integer> getCompletedCount();
|
||||
|
||||
@Insert
|
||||
void insert(Todo todo);
|
||||
|
||||
@Update
|
||||
void update(Todo todo);
|
||||
|
||||
@Delete
|
||||
void delete(Todo todo);
|
||||
|
||||
@Query("SELECT * FROM todos WHERE id = :id")
|
||||
Todo getTodoById(long id);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.todojava.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
|
||||
@Database(entities = {Todo.class}, version = 1, exportSchema = false)
|
||||
public abstract class TodoDatabase extends RoomDatabase {
|
||||
|
||||
public abstract TodoDao todoDao();
|
||||
|
||||
private static volatile TodoDatabase INSTANCE;
|
||||
|
||||
public static TodoDatabase getInstance(Context context) {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (TodoDatabase.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = Room.databaseBuilder(
|
||||
context.getApplicationContext(),
|
||||
TodoDatabase.class,
|
||||
"todo_database"
|
||||
).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#43A047" />
|
||||
<size
|
||||
android:width="12dp"
|
||||
android:height="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/primary"
|
||||
app:title="@string/toolbar_title"
|
||||
app:titleTextColor="@color/on_primary" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/filterChipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true"
|
||||
app:chipSpacingHorizontal="8dp">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipAll"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/filter_all"
|
||||
android:checked="true" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipActive"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/filter_active" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipDone"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/filter_done" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp"
|
||||
tools:listitem="@layout/item_todo" />
|
||||
|
||||
<include
|
||||
android:id="@+id/emptyState"
|
||||
layout="@layout/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/add_todo"
|
||||
app:srcCompat="@android:drawable/ic_input_add"
|
||||
app:tint="@color/on_primary"
|
||||
app:backgroundTint="@color/primary" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/titleInputLayout"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/title_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textCapSentences"
|
||||
android:maxLines="2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_label"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/on_surface"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/categoryChipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true"
|
||||
app:chipSpacingHorizontal="8dp">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipPersonal"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_personal"
|
||||
android:checked="true" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipWork"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_work" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipShopping"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_shopping" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipHealth"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_health" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipLearning"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/category_learning" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/priority_label"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/on_surface"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/priorityChipGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true"
|
||||
app:chipSpacingHorizontal="8dp">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipLow"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/priority_low" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipMedium"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/priority_medium"
|
||||
android:checked="true" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipHigh"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/priority_high" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="32dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@android:drawable/ic_menu_agenda"
|
||||
android:alpha="0.4"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_state_title"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/on_surface"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptySubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_state_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/on_surface_variant"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/on_surface"
|
||||
tools:text="Buy groceries" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipCategory"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
app:chipMinHeight="24dp"
|
||||
app:chipMinTouchTargetSize="0dp"
|
||||
tools:text="Shopping" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/priorityDot"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/priority_dot" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
After Width: | Height: | Size: 164 B |
|
After Width: | Height: | Size: 122 B |
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 413 B |
|
After Width: | Height: | Size: 546 B |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#6200EE</color>
|
||||
<color name="primary_variant">#3700B3</color>
|
||||
<color name="on_primary">#FFFFFF</color>
|
||||
<color name="secondary">#03DAC6</color>
|
||||
<color name="on_secondary">#000000</color>
|
||||
<color name="background">#FAFAFA</color>
|
||||
<color name="surface">#FFFFFF</color>
|
||||
<color name="on_surface">#1C1B1F</color>
|
||||
<color name="on_surface_variant">#49454F</color>
|
||||
|
||||
<color name="priority_low">#43A047</color>
|
||||
<color name="priority_medium">#FB8C00</color>
|
||||
<color name="priority_high">#E53935</color>
|
||||
|
||||
<color name="completed_text">#9E9E9E</color>
|
||||
<color name="divider">#E0E0E0</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Todo Java</string>
|
||||
<string name="toolbar_title">Todos \u2013 Java Android</string>
|
||||
<string name="filter_all">All</string>
|
||||
<string name="filter_active">Active</string>
|
||||
<string name="filter_done">Done</string>
|
||||
<string name="add_todo">Add Todo</string>
|
||||
<string name="edit_todo">Edit Todo</string>
|
||||
<string name="title_hint">What needs to be done?</string>
|
||||
<string name="category_label">Category</string>
|
||||
<string name="priority_label">Priority</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="empty_state_title">No todos yet</string>
|
||||
<string name="empty_state_subtitle">Tap the + button to add your first todo</string>
|
||||
<string name="empty_active_title">All done!</string>
|
||||
<string name="empty_active_subtitle">No active todos remaining</string>
|
||||
<string name="empty_done_title">Nothing completed</string>
|
||||
<string name="empty_done_subtitle">Complete a todo to see it here</string>
|
||||
<string name="category_personal">Personal</string>
|
||||
<string name="category_work">Work</string>
|
||||
<string name="category_shopping">Shopping</string>
|
||||
<string name="category_health">Health</string>
|
||||
<string name="category_learning">Learning</string>
|
||||
<string name="priority_low">Low</string>
|
||||
<string name="priority_medium">Medium</string>
|
||||
<string name="priority_high">High</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="todo_deleted">Todo deleted</string>
|
||||
<string name="undo">Undo</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.TodoJava" parent="Theme.Material3.Light.NoActionBar">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryVariant">@color/primary_variant</item>
|
||||
<item name="colorOnPrimary">@color/on_primary</item>
|
||||
<item name="colorSecondary">@color/secondary</item>
|
||||
<item name="colorOnSecondary">@color/on_secondary</item>
|
||||
<item name="android:colorBackground">@color/background</item>
|
||||
<item name="colorSurface">@color/surface</item>
|
||||
<item name="colorOnSurface">@color/on_surface</item>
|
||||
<item name="colorOnSurfaceVariant">@color/on_surface_variant</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.todokotlin"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.todokotlin"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.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)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="Todo Kotlin"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TodoKotlin">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.TodoKotlin">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.todokotlin
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.todokotlin.ui.TodoApp
|
||||
import com.example.todokotlin.ui.theme.TodoKotlinTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
TodoKotlinTheme {
|
||||
val viewModel: TodoViewModel = viewModel()
|
||||
TodoApp(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.todokotlin
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.todokotlin.data.Todo
|
||||
import com.example.todokotlin.data.TodoDatabase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class TodoFilter { All, Active, Done }
|
||||
|
||||
data class TodoUiState(
|
||||
val todos: List<Todo> = emptyList(),
|
||||
val filter: TodoFilter = TodoFilter.All,
|
||||
val allCount: Int = 0,
|
||||
val activeCount: Int = 0,
|
||||
val doneCount: Int = 0
|
||||
)
|
||||
|
||||
class TodoViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val dao = TodoDatabase.getDatabase(application).todoDao()
|
||||
private val _filter = MutableStateFlow(TodoFilter.All)
|
||||
|
||||
val uiState: StateFlow<TodoUiState> = combine(
|
||||
dao.getAllTodos(),
|
||||
_filter
|
||||
) { todos, filter ->
|
||||
val filtered = when (filter) {
|
||||
TodoFilter.All -> todos
|
||||
TodoFilter.Active -> todos.filter { !it.completed }
|
||||
TodoFilter.Done -> todos.filter { it.completed }
|
||||
}
|
||||
TodoUiState(
|
||||
todos = filtered,
|
||||
filter = filter,
|
||||
allCount = todos.size,
|
||||
activeCount = todos.count { !it.completed },
|
||||
doneCount = todos.count { it.completed }
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = TodoUiState()
|
||||
)
|
||||
|
||||
fun setFilter(filter: TodoFilter) {
|
||||
_filter.value = filter
|
||||
}
|
||||
|
||||
fun addTodo(title: String, category: String, priority: String) {
|
||||
viewModelScope.launch {
|
||||
dao.insert(
|
||||
Todo(
|
||||
title = title,
|
||||
category = category,
|
||||
priority = priority
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTodo(todo: Todo) {
|
||||
viewModelScope.launch {
|
||||
dao.update(todo)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCompleted(todo: Todo) {
|
||||
viewModelScope.launch {
|
||||
dao.update(todo.copy(completed = !todo.completed))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTodo(todo: Todo) {
|
||||
viewModelScope.launch {
|
||||
dao.delete(todo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.todokotlin.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "todos")
|
||||
data class Todo(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val title: String,
|
||||
val completed: Boolean = false,
|
||||
val category: String = "Personal",
|
||||
val priority: String = "Medium",
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.todokotlin.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TodoDao {
|
||||
@Query("SELECT * FROM todos ORDER BY createdAt DESC")
|
||||
fun getAllTodos(): Flow<List<Todo>>
|
||||
|
||||
@Insert
|
||||
suspend fun insert(todo: Todo)
|
||||
|
||||
@Update
|
||||
suspend fun update(todo: Todo)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(todo: Todo)
|
||||
|
||||
@Query("DELETE FROM todos WHERE id = :id")
|
||||
suspend fun deleteById(id: Long)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.todokotlin.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [Todo::class], version = 1, exportSchema = false)
|
||||
abstract class TodoDatabase : RoomDatabase() {
|
||||
abstract fun todoDao(): TodoDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: TodoDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): TodoDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
TodoDatabase::class.java,
|
||||
"todo_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val categories = listOf("Personal", "Work", "Shopping", "Health", "Learning")
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CategoryPicker(
|
||||
selected: String,
|
||||
onSelect: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Category",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
categories.forEach { category ->
|
||||
FilterChip(
|
||||
selected = selected == category,
|
||||
onClick = { onSelect(category) },
|
||||
label = { Text(category) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun EmptyState(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No todos yet",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Tap the + button to add your first todo",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.todokotlin.TodoFilter
|
||||
|
||||
@Composable
|
||||
fun FilterBar(
|
||||
currentFilter: TodoFilter,
|
||||
allCount: Int,
|
||||
activeCount: Int,
|
||||
doneCount: Int,
|
||||
onFilterChange: (TodoFilter) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = currentFilter == TodoFilter.All,
|
||||
onClick = { onFilterChange(TodoFilter.All) },
|
||||
label = { Text("All ($allCount)") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentFilter == TodoFilter.Active,
|
||||
onClick = { onFilterChange(TodoFilter.Active) },
|
||||
label = { Text("Active ($activeCount)") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentFilter == TodoFilter.Done,
|
||||
onClick = { onFilterChange(TodoFilter.Done) },
|
||||
label = { Text("Done ($doneCount)") }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class PriorityOption(val name: String, val color: Color)
|
||||
|
||||
val priorities = listOf(
|
||||
PriorityOption("Low", Color(0xFF43A047)),
|
||||
PriorityOption("Medium", Color(0xFFFB8C00)),
|
||||
PriorityOption("High", Color(0xFFE53935))
|
||||
)
|
||||
|
||||
fun priorityColor(priority: String): Color {
|
||||
return priorities.find { it.name == priority }?.color ?: Color(0xFFFB8C00)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PriorityPicker(
|
||||
selected: String,
|
||||
onSelect: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "Priority",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
priorities.forEach { priority ->
|
||||
FilterChip(
|
||||
selected = selected == priority.name,
|
||||
onClick = { onSelect(priority.name) },
|
||||
label = { Text(priority.name) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = priority.color.copy(alpha = 0.2f),
|
||||
selectedLabelColor = priority.color
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.example.todokotlin.TodoViewModel
|
||||
import com.example.todokotlin.data.Todo
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TodoApp(viewModel: TodoViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var editingTodo by remember { mutableStateOf<Todo?>(null) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("Todos \u2013 Kotlin Android")
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { showAddDialog = true },
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add Todo",
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
FilterBar(
|
||||
currentFilter = uiState.filter,
|
||||
allCount = uiState.allCount,
|
||||
activeCount = uiState.activeCount,
|
||||
doneCount = uiState.doneCount,
|
||||
onFilterChange = { viewModel.setFilter(it) }
|
||||
)
|
||||
|
||||
if (uiState.todos.isEmpty()) {
|
||||
EmptyState()
|
||||
} else {
|
||||
TodoList(
|
||||
todos = uiState.todos,
|
||||
onToggle = { viewModel.toggleCompleted(it) },
|
||||
onTap = { editingTodo = it },
|
||||
onDelete = { viewModel.deleteTodo(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddDialog) {
|
||||
TodoFormDialog(
|
||||
onDismiss = { showAddDialog = false },
|
||||
onSave = { title, category, priority ->
|
||||
viewModel.addTodo(title, category, priority)
|
||||
showAddDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
editingTodo?.let { todo ->
|
||||
TodoFormDialog(
|
||||
todo = todo,
|
||||
onDismiss = { editingTodo = null },
|
||||
onSave = { title, category, priority ->
|
||||
viewModel.updateTodo(
|
||||
todo.copy(
|
||||
title = title,
|
||||
category = category,
|
||||
priority = priority
|
||||
)
|
||||
)
|
||||
editingTodo = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
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.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.todokotlin.data.Todo
|
||||
|
||||
@Composable
|
||||
fun TodoFormDialog(
|
||||
todo: Todo? = null,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (title: String, category: String, priority: String) -> Unit
|
||||
) {
|
||||
var title by remember { mutableStateOf(todo?.title ?: "") }
|
||||
var category by remember { mutableStateOf(todo?.category ?: "Personal") }
|
||||
var priority by remember { mutableStateOf(todo?.priority ?: "Medium") }
|
||||
|
||||
val isEditing = todo != null
|
||||
val dialogTitle = if (isEditing) "Edit Todo" else "Add Todo"
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(dialogTitle) },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text("Title") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
CategoryPicker(
|
||||
selected = category,
|
||||
onSelect = { category = it }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
PriorityPicker(
|
||||
selected = priority,
|
||||
onSelect = { priority = it }
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (title.isNotBlank()) {
|
||||
onSave(title.trim(), category, priority)
|
||||
}
|
||||
},
|
||||
enabled = title.isNotBlank()
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.todokotlin.data.Todo
|
||||
|
||||
@Composable
|
||||
fun TodoItem(
|
||||
todo: Todo,
|
||||
onToggle: () -> Unit,
|
||||
onTap: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onTap() },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = todo.completed,
|
||||
onCheckedChange = { onToggle() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = todo.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textDecoration = if (todo.completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||
color = if (todo.completed)
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SuggestionChip(
|
||||
onClick = { },
|
||||
label = {
|
||||
Text(
|
||||
text = todo.category,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(priorityColor(todo.priority))
|
||||
)
|
||||
Text(
|
||||
text = todo.priority,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = priorityColor(todo.priority)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.example.todokotlin.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.todokotlin.data.Todo
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TodoList(
|
||||
todos: List<Todo>,
|
||||
onToggle: (Todo) -> Unit,
|
||||
onTap: (Todo) -> Unit,
|
||||
onDelete: (Todo) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = todos,
|
||||
key = { it.id }
|
||||
) { todo ->
|
||||
val dismissState = rememberSwipeToDismissBoxState(
|
||||
confirmValueChange = { value ->
|
||||
if (value == SwipeToDismissBoxValue.EndToStart) {
|
||||
onDelete(todo)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
SwipeToDismissBox(
|
||||
state = dismissState,
|
||||
backgroundContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFE53935))
|
||||
.padding(horizontal = 20.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
},
|
||||
enableDismissFromStartToEnd = false,
|
||||
enableDismissFromEndToStart = true
|
||||
) {
|
||||
TodoItem(
|
||||
todo = todo,
|
||||
onToggle = { onToggle(todo) },
|
||||
onTap = { onTap(todo) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.todokotlin.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF6200EE),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFE8DEF8),
|
||||
onPrimaryContainer = Color(0xFF21005D),
|
||||
secondary = Color(0xFF03DAC6),
|
||||
onSecondary = Color.Black,
|
||||
background = Color(0xFFFFFBFE),
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
surfaceVariant = Color(0xFFE7E0EC),
|
||||
onSurfaceVariant = Color(0xFF49454F)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TodoKotlinTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = LightColorScheme,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.TodoKotlin" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||