diff --git a/.gitignore b/.gitignore index 2534362..bd90bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ npm-debug.* # Expo / React Native .expo/ -app/ -ios/ web-build/ # Shadow-cljs / Clojure @@ -47,6 +45,8 @@ test/cljd-out/ local.properties gradle/wrapper/gradle-wrapper.jar captures/ +*.keystore +*.jks # Kotlin *.class diff --git a/todo-expo/android/app/build.gradle b/todo-expo/android/app/build.gradle new file mode 100644 index 0000000..e53f695 --- /dev/null +++ b/todo-expo/android/app/build.gradle @@ -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.. 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 + } +} diff --git a/todo-expo/android/app/proguard-rules.pro b/todo-expo/android/app/proguard-rules.pro new file mode 100644 index 0000000..551eb41 --- /dev/null +++ b/todo-expo/android/app/proguard-rules.pro @@ -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: diff --git a/todo-expo/android/app/src/debug/AndroidManifest.xml b/todo-expo/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3ec2507 --- /dev/null +++ b/todo-expo/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/todo-expo/android/app/src/debugOptimized/AndroidManifest.xml b/todo-expo/android/app/src/debugOptimized/AndroidManifest.xml new file mode 100644 index 0000000..3ec2507 --- /dev/null +++ b/todo-expo/android/app/src/debugOptimized/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/todo-expo/android/app/src/main/AndroidManifest.xml b/todo-expo/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1bc0288 --- /dev/null +++ b/todo-expo/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainActivity.kt b/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainActivity.kt new file mode 100644 index 0000000..442a2e2 --- /dev/null +++ b/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainActivity.kt @@ -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 onBackPressed + */ + 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() + } +} diff --git a/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainApplication.kt b/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainApplication.kt new file mode 100644 index 0000000..5328fad --- /dev/null +++ b/todo-expo/android/app/src/main/java/com/anonymous/todoexpojs/MainApplication.kt @@ -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) + } +} diff --git a/todo-expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/todo-expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..31df827 Binary files /dev/null and b/todo-expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/todo-expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/todo-expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..ef243aa Binary files /dev/null and b/todo-expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/todo-expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/todo-expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..e9d5474 Binary files /dev/null and b/todo-expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/todo-expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/todo-expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..d61da15 Binary files /dev/null and b/todo-expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/todo-expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/todo-expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..4aeed11 Binary files /dev/null and b/todo-expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/todo-expo/android/app/src/main/res/drawable/ic_launcher_background.xml b/todo-expo/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..883b2a0 --- /dev/null +++ b/todo-expo/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/todo-expo/android/app/src/main/res/drawable/rn_edit_text_material.xml b/todo-expo/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000..5c25e72 --- /dev/null +++ b/todo-expo/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/todo-expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/todo-expo/android/app/src/main/res/values-night/colors.xml b/todo-expo/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/todo-expo/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/todo-expo/android/app/src/main/res/values/colors.xml b/todo-expo/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..65f3b84 --- /dev/null +++ b/todo-expo/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + #FFFFFF + #023c69 + \ No newline at end of file diff --git a/todo-expo/android/app/src/main/res/values/strings.xml b/todo-expo/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..92bd140 --- /dev/null +++ b/todo-expo/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Todo App + \ No newline at end of file diff --git a/todo-expo/android/app/src/main/res/values/styles.xml b/todo-expo/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..eaf6428 --- /dev/null +++ b/todo-expo/android/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/todo-flutter/android/app/build.gradle b/todo-flutter/android/app/build.gradle new file mode 100644 index 0000000..e673efb --- /dev/null +++ b/todo-flutter/android/app/build.gradle @@ -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 = "../.." +} diff --git a/todo-flutter/android/app/src/debug/AndroidManifest.xml b/todo-flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/todo-flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/todo-flutter/android/app/src/main/AndroidManifest.xml b/todo-flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..940369d --- /dev/null +++ b/todo-flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/todo-flutter/android/app/src/main/kotlin/com/anonymous/todo_flutter/MainActivity.kt b/todo-flutter/android/app/src/main/kotlin/com/anonymous/todo_flutter/MainActivity.kt new file mode 100644 index 0000000..74534ba --- /dev/null +++ b/todo-flutter/android/app/src/main/kotlin/com/anonymous/todo_flutter/MainActivity.kt @@ -0,0 +1,5 @@ +package com.anonymous.todo_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/todo-flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/todo-flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/todo-flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/todo-flutter/android/app/src/main/res/drawable/launch_background.xml b/todo-flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/todo-flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/todo-flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/todo-flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/todo-flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/todo-flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/todo-flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/todo-flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/todo-flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/todo-flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/todo-flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/todo-flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/todo-flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/todo-flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/todo-flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/todo-flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/todo-flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/todo-flutter/android/app/src/main/res/values-night/styles.xml b/todo-flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/todo-flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/todo-flutter/android/app/src/main/res/values/styles.xml b/todo-flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/todo-flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/todo-flutter/android/app/src/profile/AndroidManifest.xml b/todo-flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/todo-flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/todo-java/app/build.gradle.kts b/todo-java/app/build.gradle.kts new file mode 100644 index 0000000..f137595 --- /dev/null +++ b/todo-java/app/build.gradle.kts @@ -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) +} diff --git a/todo-java/app/src/main/AndroidManifest.xml b/todo-java/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd1d53c --- /dev/null +++ b/todo-java/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/todo-java/app/src/main/java/com/example/todojava/MainActivity.java b/todo-java/app/src/main/java/com/example/todojava/MainActivity.java new file mode 100644 index 0000000..b1020f4 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/MainActivity.java @@ -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 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; + } + } + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/SwipeToDeleteCallback.java b/todo-java/app/src/main/java/com/example/todojava/SwipeToDeleteCallback.java new file mode 100644 index 0000000..9f03271 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/SwipeToDeleteCallback.java @@ -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); + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/TodoAdapter.java b/todo-java/app/src/main/java/com/example/todojava/TodoAdapter.java new file mode 100644 index 0000000..081b35f --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/TodoAdapter.java @@ -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 { + + 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 DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @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); + } + }); + } + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/TodoFormDialogFragment.java b/todo-java/app/src/main/java/com/example/todojava/TodoFormDialogFragment.java new file mode 100644 index 0000000..52dea18 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/TodoFormDialogFragment.java @@ -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; + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/TodoViewModel.java b/todo-java/app/src/main/java/com/example/todojava/TodoViewModel.java new file mode 100644 index 0000000..730f4f1 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/TodoViewModel.java @@ -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> allTodos; + private final LiveData> activeTodos; + private final LiveData> completedTodos; + + private final LiveData allCount; + private final LiveData activeCount; + private final LiveData completedCount; + + private final MutableLiveData currentFilter = new MutableLiveData<>(Filter.ALL); + private final MediatorLiveData> 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> getFilteredTodos() { + return filteredTodos; + } + + public LiveData getAllCount() { + return allCount; + } + + public LiveData getActiveCount() { + return activeCount; + } + + public LiveData getCompletedCount() { + return completedCount; + } + + public MutableLiveData 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)); + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/data/Todo.java b/todo-java/app/src/main/java/com/example/todojava/data/Todo.java new file mode 100644 index 0000000..29f7d43 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/data/Todo.java @@ -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; + } +} diff --git a/todo-java/app/src/main/java/com/example/todojava/data/TodoDao.java b/todo-java/app/src/main/java/com/example/todojava/data/TodoDao.java new file mode 100644 index 0000000..de5945c --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/data/TodoDao.java @@ -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> getAllTodos(); + + @Query("SELECT * FROM todos WHERE completed = 0 ORDER BY created_at DESC") + LiveData> getActiveTodos(); + + @Query("SELECT * FROM todos WHERE completed = 1 ORDER BY created_at DESC") + LiveData> getCompletedTodos(); + + @Query("SELECT COUNT(*) FROM todos") + LiveData getAllCount(); + + @Query("SELECT COUNT(*) FROM todos WHERE completed = 0") + LiveData getActiveCount(); + + @Query("SELECT COUNT(*) FROM todos WHERE completed = 1") + LiveData 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); +} diff --git a/todo-java/app/src/main/java/com/example/todojava/data/TodoDatabase.java b/todo-java/app/src/main/java/com/example/todojava/data/TodoDatabase.java new file mode 100644 index 0000000..7f731c5 --- /dev/null +++ b/todo-java/app/src/main/java/com/example/todojava/data/TodoDatabase.java @@ -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; + } +} diff --git a/todo-java/app/src/main/res/drawable/priority_dot.xml b/todo-java/app/src/main/res/drawable/priority_dot.xml new file mode 100644 index 0000000..fdf3a4c --- /dev/null +++ b/todo-java/app/src/main/res/drawable/priority_dot.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/todo-java/app/src/main/res/layout/activity_main.xml b/todo-java/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..9be43ce --- /dev/null +++ b/todo-java/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/todo-java/app/src/main/res/layout/dialog_todo_form.xml b/todo-java/app/src/main/res/layout/dialog_todo_form.xml new file mode 100644 index 0000000..c4aeef2 --- /dev/null +++ b/todo-java/app/src/main/res/layout/dialog_todo_form.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/todo-java/app/src/main/res/layout/empty_state.xml b/todo-java/app/src/main/res/layout/empty_state.xml new file mode 100644 index 0000000..c840554 --- /dev/null +++ b/todo-java/app/src/main/res/layout/empty_state.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/todo-java/app/src/main/res/layout/item_todo.xml b/todo-java/app/src/main/res/layout/item_todo.xml new file mode 100644 index 0000000..9bf62ee --- /dev/null +++ b/todo-java/app/src/main/res/layout/item_todo.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/todo-java/app/src/main/res/mipmap-hdpi/ic_launcher.png b/todo-java/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cf826d7 Binary files /dev/null and b/todo-java/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/todo-java/app/src/main/res/mipmap-mdpi/ic_launcher.png b/todo-java/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..42209cf Binary files /dev/null and b/todo-java/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/todo-java/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/todo-java/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3de2d40 Binary files /dev/null and b/todo-java/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/todo-java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/todo-java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..00b5db7 Binary files /dev/null and b/todo-java/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/todo-java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/todo-java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..0c65c3c Binary files /dev/null and b/todo-java/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/todo-java/app/src/main/res/values/colors.xml b/todo-java/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..1b6691d --- /dev/null +++ b/todo-java/app/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + #6200EE + #3700B3 + #FFFFFF + #03DAC6 + #000000 + #FAFAFA + #FFFFFF + #1C1B1F + #49454F + + #43A047 + #FB8C00 + #E53935 + + #9E9E9E + #E0E0E0 + diff --git a/todo-java/app/src/main/res/values/strings.xml b/todo-java/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d849cff --- /dev/null +++ b/todo-java/app/src/main/res/values/strings.xml @@ -0,0 +1,32 @@ + + + Todo Java + Todos \u2013 Java Android + All + Active + Done + Add Todo + Edit Todo + What needs to be done? + Category + Priority + Save + Cancel + No todos yet + Tap the + button to add your first todo + All done! + No active todos remaining + Nothing completed + Complete a todo to see it here + Personal + Work + Shopping + Health + Learning + Low + Medium + High + Delete + Todo deleted + Undo + diff --git a/todo-java/app/src/main/res/values/themes.xml b/todo-java/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..fc63bf7 --- /dev/null +++ b/todo-java/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/todo-kotlin/app/build.gradle.kts b/todo-kotlin/app/build.gradle.kts new file mode 100644 index 0000000..844934c --- /dev/null +++ b/todo-kotlin/app/build.gradle.kts @@ -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) +} diff --git a/todo-kotlin/app/src/main/AndroidManifest.xml b/todo-kotlin/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ed7a385 --- /dev/null +++ b/todo-kotlin/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/MainActivity.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/MainActivity.kt new file mode 100644 index 0000000..c9da002 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/MainActivity.kt @@ -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) + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/TodoViewModel.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/TodoViewModel.kt new file mode 100644 index 0000000..c1fbb34 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/TodoViewModel.kt @@ -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 = 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 = 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) + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/data/Todo.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/Todo.kt new file mode 100644 index 0000000..daee3fa --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/Todo.kt @@ -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() +) diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDao.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDao.kt new file mode 100644 index 0000000..1d0f281 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDao.kt @@ -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> + + @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) +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDatabase.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDatabase.kt new file mode 100644 index 0000000..0d4d368 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/data/TodoDatabase.kt @@ -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 + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/CategoryPicker.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/CategoryPicker.kt new file mode 100644 index 0000000..9f5c68b --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/CategoryPicker.kt @@ -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) } + ) + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/EmptyState.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/EmptyState.kt new file mode 100644 index 0000000..d47b61c --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/EmptyState.kt @@ -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 + ) + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/FilterBar.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/FilterBar.kt new file mode 100644 index 0000000..7e485d0 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/FilterBar.kt @@ -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)") } + ) + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/PriorityPicker.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/PriorityPicker.kt new file mode 100644 index 0000000..3fc1da8 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/PriorityPicker.kt @@ -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 + ) + ) + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoApp.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoApp.kt new file mode 100644 index 0000000..de1cd0b --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoApp.kt @@ -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(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 + } + ) + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoFormDialog.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoFormDialog.kt new file mode 100644 index 0000000..fa1ad7a --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoFormDialog.kt @@ -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") + } + } + ) +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoItem.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoItem.kt new file mode 100644 index 0000000..e7bc523 --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoItem.kt @@ -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) + ) + } + } + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoList.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoList.kt new file mode 100644 index 0000000..b7ed32d --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/TodoList.kt @@ -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, + 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) } + ) + } + } + } +} diff --git a/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/theme/Theme.kt b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/theme/Theme.kt new file mode 100644 index 0000000..e657d6a --- /dev/null +++ b/todo-kotlin/app/src/main/java/com/example/todokotlin/ui/theme/Theme.kt @@ -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 + ) +} diff --git a/todo-kotlin/app/src/main/res/values/themes.xml b/todo-kotlin/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4edb732 --- /dev/null +++ b/todo-kotlin/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +