fix gitignore
@@ -13,8 +13,6 @@ npm-debug.*
|
|||||||
|
|
||||||
# Expo / React Native
|
# Expo / React Native
|
||||||
.expo/
|
.expo/
|
||||||
app/
|
|
||||||
ios/
|
|
||||||
web-build/
|
web-build/
|
||||||
|
|
||||||
# Shadow-cljs / Clojure
|
# Shadow-cljs / Clojure
|
||||||
@@ -47,6 +45,8 @@ test/cljd-out/
|
|||||||
local.properties
|
local.properties
|
||||||
gradle/wrapper/gradle-wrapper.jar
|
gradle/wrapper/gradle-wrapper.jar
|
||||||
captures/
|
captures/
|
||||||
|
*.keystore
|
||||||
|
*.jks
|
||||||
|
|
||||||
# Kotlin
|
# Kotlin
|
||||||
*.class
|
*.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>
|
||||||