diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7750ad13 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## June 11, 2019 + +* Added dependency injection +* Refactored dio client diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..aaaf6208 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Zubair Rehman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d99f6c4c..c3a22f7e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -# Smart-Insti-App 📱🏫 -This app aims to solve the day-to-day problems that students and faculty face in IIT Bhilai and aims to consolidate a lot of useful applications into single app. This could include features like Time Table, Classroom Vacancy, Lost and Found, Chatrooms on various topics like Internet Issues. It could also have a broadcast feature which would be very useful in emergency situations. - -## Techstack 👩‍💻 -Flutter, Firebase - -## Maintainers ☀️ - |[@Ananya](https://github.com/Ananyaiitbhilai)|[@chaitanyabisht](https://github.com/chaitanyabisht) |[@Maanas Talwar](https://github.com/maanas-talwar)| [@Satvik Vemuganti](https://github.com/VickyMerzOwn)| -|------|-------|---------|--------| - -## Other details 📑 -This project is in its first iteration hence have to be started from scratch. A basic experience in app development is preferred. - -## Contribution Guidelines ✨ -Please go through the Wiki. Even a small contribution helps. All forms of contributions including documentation changes are highly welcomed and valued. -- [How to create a PR?](https://github.com/OpenLake/Not-a-Mess/wiki/How-to-create-a-PR%3F) -- [PR Format Guidelines](https://github.com/OpenLake/Not-a-Mess/wiki/PR-Format-Guidelines) +# smart_insti + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..a7acf245 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..383c6726 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,71 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.smart_insti" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + 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 '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..6d680701 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b1593b0f --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/iotecksolutions/todoapp/MainActivity.java b/android/app/src/main/java/com/iotecksolutions/todoapp/MainActivity.java new file mode 100644 index 00000000..1e184251 --- /dev/null +++ b/android/app/src/main/java/com/iotecksolutions/todoapp/MainActivity.java @@ -0,0 +1,7 @@ +package com.iotecksolutions.todoapp; + +import io.flutter.embedding.android.FlutterActivity; + +public class MainActivity extends FlutterActivity { + +} diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 00000000..539ab022 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,19 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + } +} diff --git a/android/app/src/main/kotlin/com/example/smart_insti/MainActivity.kt b/android/app/src/main/kotlin/com/example/smart_insti/MainActivity.kt new file mode 100644 index 00000000..439ff0d1 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/smart_insti/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.smart_insti + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..1cb7aa2f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_appicon.png b/android/app/src/main/res/drawable/ic_appicon.png new file mode 100644 index 00000000..655cd0e0 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_appicon.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..84037589 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..360a1605 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5fac6796 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..6d680701 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..104a4864 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..46c1f169 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13372aef Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2108ceb9 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 00000000..9d82f789 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/local.properties b/android/local.properties new file mode 100644 index 00000000..9c05e2dc --- /dev/null +++ b/android/local.properties @@ -0,0 +1 @@ +flutter.sdk=F:\\flutter \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..33f0745d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/android/smart_insti_android.iml b/android/smart_insti_android.iml new file mode 100644 index 00000000..5e74ee6a --- /dev/null +++ b/android/smart_insti_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/art/architecture.png b/art/architecture.png new file mode 100644 index 00000000..ad94e3bb Binary files /dev/null and b/art/architecture.png differ diff --git a/art/flow.png b/art/flow.png new file mode 100644 index 00000000..f7293596 Binary files /dev/null and b/art/flow.png differ diff --git a/assets/fonts/Product-Sans-Bold-Italic.ttf b/assets/fonts/Product-Sans-Bold-Italic.ttf new file mode 100644 index 00000000..129d12df Binary files /dev/null and b/assets/fonts/Product-Sans-Bold-Italic.ttf differ diff --git a/assets/fonts/Product-Sans-Bold.ttf b/assets/fonts/Product-Sans-Bold.ttf new file mode 100644 index 00000000..d847195c Binary files /dev/null and b/assets/fonts/Product-Sans-Bold.ttf differ diff --git a/assets/fonts/Product-Sans-Italic.ttf b/assets/fonts/Product-Sans-Italic.ttf new file mode 100644 index 00000000..5fc56d4c Binary files /dev/null and b/assets/fonts/Product-Sans-Italic.ttf differ diff --git a/assets/fonts/Product-Sans-Regular.ttf b/assets/fonts/Product-Sans-Regular.ttf new file mode 100644 index 00000000..c0442ee2 Binary files /dev/null and b/assets/fonts/Product-Sans-Regular.ttf differ diff --git a/assets/icons/ic_appicon.png b/assets/icons/ic_appicon.png new file mode 100644 index 00000000..655cd0e0 Binary files /dev/null and b/assets/icons/ic_appicon.png differ diff --git a/assets/icons/ic_launcher.png b/assets/icons/ic_launcher.png new file mode 100644 index 00000000..285962f4 Binary files /dev/null and b/assets/icons/ic_launcher.png differ diff --git a/assets/images/img_login.jpg b/assets/images/img_login.jpg new file mode 100644 index 00000000..5e8529f1 Binary files /dev/null and b/assets/images/img_login.jpg differ diff --git a/assets/images/img_no_jobs.png b/assets/images/img_no_jobs.png new file mode 100644 index 00000000..a5c716d8 Binary files /dev/null and b/assets/images/img_no_jobs.png differ diff --git a/assets/lang/da.json b/assets/lang/da.json new file mode 100644 index 00000000..064bb1a1 --- /dev/null +++ b/assets/lang/da.json @@ -0,0 +1,17 @@ +{ + "login_start": "Below are list of strings for login da ", + "login_et_user_email": "Brugernavn", + "login_et_user_password": "Adgangskode", + "login_btn_forgot_password": "Glemt adgangskode?", + "login_btn_sign_in": "Log ind", + "login_error_fill_fields": "Udfyld venligst alle felterne", + "login_end": "------------------------------------------------------------------------------------", + + "home_start": "Below are list of strings for home", + "home_tv_posts": "Indlæg", + "home_tv_error": "Fejl", + "home_tv_no_post_found": "Ingen indlæg fundet", + "home_tv_choose_language": "Vælg sprog", + "home_end": "-------------------------------------------------------------------------------------" + +} diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 00000000..1be1f370 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,17 @@ +{ + "login_start": "Below are list of strings for login", + "login_et_user_email": "Enter user email", + "login_et_user_password": "Enter password", + "login_btn_forgot_password": "Forgot Password?", + "login_btn_sign_in": "Sign In", + "login_error_fill_fields": "Please fill in all fields", + "login_end": "------------------------------------------------------------------------------------", + + "home_start": "Below are list of strings for home", + "home_tv_posts": "Posts", + "home_tv_error": "Error", + "home_tv_no_post_found": "No posts found", + "home_tv_choose_language": "Choose Language", + "home_end": "-------------------------------------------------------------------------------------" + +} diff --git a/assets/lang/es.json b/assets/lang/es.json new file mode 100644 index 00000000..d4fc47f6 --- /dev/null +++ b/assets/lang/es.json @@ -0,0 +1,16 @@ +{ + "login_start": "Below are list of strings for login es ", + "login_et_user_email": "Ingrese el correo electrónico del usuario", + "login_et_user_password": "Introducir la contraseña", + "login_btn_forgot_password": "¿Se te olvidó tu contraseña", + "login_btn_sign_in": "Registrarse", + "login_error_fill_fields": "Por favor complete todos los campos e", + "login_end": "------------------------------------------------------------------------------------", + + "home_start": "Below are list of strings for home", + "home_tv_posts": "Publicaciones", + "home_tv_error": "Error", + "home_tv_no_post_found": "No se han encontrado publicaciones", + "home_tv_choose_language": "Elige lengua", + "home_end": "-------------------------------------------------------------------------------------" +} diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..8d4492f9 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c83b0051 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,539 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 8981BC204AA6763F48392668 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 887069A31D31C9BF9D217F54 /* libPods-Runner.a */; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4FA17A1567654C183BDA1E63 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 86CC2D58A4D5E9C5BEA67555 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 887069A31D31C9BF9D217F54 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5E67463C84E3E6657585186 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8981BC204AA6763F48392668 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6A50AD2F0B3F0D889EFA460A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 887069A31D31C9BF9D217F54 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + C48FD86A9715636178099FE1 /* Pods */, + 6A50AD2F0B3F0D889EFA460A /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + C48FD86A9715636178099FE1 /* Pods */ = { + isa = PBXGroup; + children = ( + B5E67463C84E3E6657585186 /* Pods-Runner.debug.xcconfig */, + 4FA17A1567654C183BDA1E63 /* Pods-Runner.release.xcconfig */, + 86CC2D58A4D5E9C5BEA67555 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 641FA095A70B88123015AA6E /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 641FA095A70B88123015AA6E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.iotecksolutions.todoApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.iotecksolutions.todoApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.iotecksolutions.todoApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..f1560181 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h new file mode 100644 index 00000000..36e21bbf --- /dev/null +++ b/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m new file mode 100644 index 00000000..59a72e90 --- /dev/null +++ b/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..3d43d11e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 00000000..25f900c7 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,50 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Boilerplate Project + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/main.m b/ios/Runner/main.m new file mode 100644 index 00000000..dff6597e --- /dev/null +++ b/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/lib/constants/app_theme.dart b/lib/constants/app_theme.dart new file mode 100644 index 00000000..fec93925 --- /dev/null +++ b/lib/constants/app_theme.dart @@ -0,0 +1,112 @@ +/// Creating custom color palettes is part of creating a custom app. The idea is to create +/// your class of custom colors, in this case `CompanyColors` and then create a `ThemeData` +/// object with those colors you just defined. +/// +/// Resource: +/// A good resource would be this website: http://mcg.mbitson.com/ +/// You simply need to put in the colour you wish to use, and it will generate all shades +/// for you. Your primary colour will be the `500` value. +/// +/// Colour Creation: +/// In order to create the custom colours you need to create a `Map` object +/// which will have all the shade values. `const Color(0xFF...)` will be how you create +/// the colours. The six character hex code is what follows. If you wanted the colour +/// #114488 or #D39090 as primary colours in your theme, then you would have +/// `const Color(0x114488)` and `const Color(0xD39090)`, respectively. +/// +/// Usage: +/// In order to use this newly created theme or even the colours in it, you would just +/// `import` this file in your project, anywhere you needed it. +/// `import 'path/to/theme.dart';` +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppThemeData { + static const _lightFillColor = Colors.black; + static const _darkFillColor = Colors.white; + + static final Color _lightFocusColor = Colors.black.withOpacity(0.12); + static final Color _darkFocusColor = Colors.white.withOpacity(0.12); + + static ThemeData lightThemeData = + themeData(lightColorScheme, _lightFocusColor); + static ThemeData darkThemeData = themeData(darkColorScheme, _darkFocusColor); + + static ThemeData themeData(ColorScheme colorScheme, Color focusColor) { + return ThemeData( + colorScheme: colorScheme, + textTheme: _textTheme, + // Matches manifest.json colors and background color. + primaryColor: const Color(0xFF030303), + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.background, + elevation: 0, + iconTheme: IconThemeData(color: colorScheme.primary), + ), + iconTheme: IconThemeData(color: colorScheme.onPrimary), + canvasColor: colorScheme.background, + scaffoldBackgroundColor: colorScheme.background, + highlightColor: Colors.transparent, + focusColor: focusColor, + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: Color.alphaBlend( + _lightFillColor.withOpacity(0.80), + _darkFillColor, + ), + contentTextStyle: _textTheme.subtitle1!.apply(color: _darkFillColor), + ), + ); + } + + static const ColorScheme lightColorScheme = ColorScheme( + primary: Color(0xFFd21e1d), + primaryContainer: Color(0xFF9e1718), + secondary: Color(0xFFEFF3F3), + secondaryContainer: Color(0xFFFAFBFB), + background: Color(0xFFE6EBEB), + surface: Color(0xFFFAFBFB), + onBackground: Colors.white, + error: _lightFillColor, + onError: _lightFillColor, + onPrimary: _lightFillColor, + onSecondary: Color(0xFF322942), + onSurface: Color(0xFF241E30), + brightness: Brightness.light, + ); + + static const ColorScheme darkColorScheme = ColorScheme( + primary: Color(0xFFFF8383), + primaryContainer: Color(0xFF1CDEC9), + secondary: Color(0xFF4D1F7C), + secondaryContainer: Color(0xFF451B6F), + background: Color(0xFF241E30), + surface: Color(0xFF1F1929), + onBackground: Color(0x0DFFFFFF), + // White with 0.05 opacity + error: _darkFillColor, + onError: _darkFillColor, + onPrimary: _darkFillColor, + onSecondary: _darkFillColor, + onSurface: _darkFillColor, + brightness: Brightness.dark, + ); + + static const _regular = FontWeight.w400; + static const _medium = FontWeight.w500; + static const _semiBold = FontWeight.w600; + static const _bold = FontWeight.w700; + + static final TextTheme _textTheme = TextTheme( + headline4: GoogleFonts.montserrat(fontWeight: _bold, fontSize: 20.0), + caption: GoogleFonts.oswald(fontWeight: _semiBold, fontSize: 16.0), + headline5: GoogleFonts.oswald(fontWeight: _medium, fontSize: 16.0), + subtitle1: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 16.0), + overline: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 12.0), + bodyText1: GoogleFonts.montserrat(fontWeight: _regular, fontSize: 14.0), + subtitle2: GoogleFonts.montserrat(fontWeight: _medium, fontSize: 14.0), + bodyText2: GoogleFonts.montserrat(fontWeight: _regular, fontSize: 16.0), + headline6: GoogleFonts.montserrat(fontWeight: _bold, fontSize: 16.0), + button: GoogleFonts.montserrat(fontWeight: _semiBold, fontSize: 14.0), + ); +} diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart new file mode 100644 index 00000000..4d8505c7 --- /dev/null +++ b/lib/constants/assets.dart @@ -0,0 +1,10 @@ +class Assets { + Assets._(); + + // splash screen assets + static const String appLogo = "assets/icons/ic_appicon.png"; + + // login screen assets + static const String carBackground = "assets/images/img_login.jpg"; + +} \ No newline at end of file diff --git a/lib/constants/colors.dart b/lib/constants/colors.dart new file mode 100644 index 00000000..9cecce17 --- /dev/null +++ b/lib/constants/colors.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class AppColors { + AppColors._(); // this basically makes it so you can't instantiate this class + + static const Map orange = const { + 50: const Color(0xFFFCF2E7), + 100: const Color(0xFFF8DEC3), + 200: const Color(0xFFF3C89C), + 300: const Color(0xFFEEB274), + 400: const Color(0xFFEAA256), + 500: const Color(0xFFE69138), + 600: const Color(0xFFE38932), + 700: const Color(0xFFDF7E2B), + 800: const Color(0xFFDB7424), + 900: const Color(0xFFD56217) + }; +} diff --git a/lib/constants/dimens.dart b/lib/constants/dimens.dart new file mode 100644 index 00000000..20b774d9 --- /dev/null +++ b/lib/constants/dimens.dart @@ -0,0 +1,7 @@ +class Dimens { + Dimens._(); + + //for all screens + static const double horizontal_padding = 12.0; + static const double vertical_padding = 12.0; +} \ No newline at end of file diff --git a/lib/constants/font_family.dart b/lib/constants/font_family.dart new file mode 100644 index 00000000..69001996 --- /dev/null +++ b/lib/constants/font_family.dart @@ -0,0 +1,6 @@ +class FontFamily { + FontFamily._(); + + static String productSans = "ProductSans"; + static String roboto = "Roboto"; +} \ No newline at end of file diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart new file mode 100644 index 00000000..04bd63b6 --- /dev/null +++ b/lib/constants/strings.dart @@ -0,0 +1,6 @@ +class Strings { + Strings._(); + + //General + static const String appName = "Boilerplate Project"; +} diff --git a/lib/data/local/constants/db_constants.dart b/lib/data/local/constants/db_constants.dart new file mode 100644 index 00000000..25ba1b03 --- /dev/null +++ b/lib/data/local/constants/db_constants.dart @@ -0,0 +1,12 @@ +class DBConstants { + DBConstants._(); + + // Store Name + static const String STORE_NAME = 'demo'; + + // DB Name + static const DB_NAME = 'demo.db'; + + // Fields + static const FIELD_ID = 'id'; +} diff --git a/lib/data/local/datasources/post/post_datasource.dart b/lib/data/local/datasources/post/post_datasource.dart new file mode 100644 index 00000000..8d1218a0 --- /dev/null +++ b/lib/data/local/datasources/post/post_datasource.dart @@ -0,0 +1,101 @@ +import 'package:boilerplate/data/local/constants/db_constants.dart'; +import 'package:boilerplate/models/post/post.dart'; +import 'package:boilerplate/models/post/post_list.dart'; +import 'package:sembast/sembast.dart'; + +class PostDataSource { + // A Store with int keys and Map values. + // This Store acts like a persistent map, values of which are Flogs objects converted to Map + final _postsStore = intMapStoreFactory.store(DBConstants.STORE_NAME); + + // Private getter to shorten the amount of code needed to get the + // singleton instance of an opened database. +// Future get _db async => await AppDatabase.instance.database; + + // database instance + final Database _db; + + // Constructor + PostDataSource(this._db); + + // DB functions:-------------------------------------------------------------- + Future insert(Post post) async { + return await _postsStore.add(_db, post.toMap()); + } + + Future count() async { + return await _postsStore.count(_db); + } + + Future> getAllSortedByFilter({List? filters}) async { + //creating finder + final finder = Finder( + filter: filters != null ? Filter.and(filters) : null, + sortOrders: [SortOrder(DBConstants.FIELD_ID)]); + + final recordSnapshots = await _postsStore.find( + _db, + finder: finder, + ); + + // Making a List out of List + return recordSnapshots.map((snapshot) { + final post = Post.fromMap(snapshot.value); + // An ID is a key of a record from the database. + post.id = snapshot.key; + return post; + }).toList(); + } + + Future getPostsFromDb() async { + + print('Loading from database'); + + // post list + var postsList; + + // fetching data + final recordSnapshots = await _postsStore.find( + _db, + ); + + // Making a List out of List + if(recordSnapshots.length > 0) { + postsList = PostList( + posts: recordSnapshots.map((snapshot) { + final post = Post.fromMap(snapshot.value); + // An ID is a key of a record from the database. + post.id = snapshot.key; + return post; + }).toList()); + } + + return postsList; + } + + Future update(Post post) async { + // For filtering by key (ID), RegEx, greater than, and many other criteria, + // we use a Finder. + final finder = Finder(filter: Filter.byKey(post.id)); + return await _postsStore.update( + _db, + post.toMap(), + finder: finder, + ); + } + + Future delete(Post post) async { + final finder = Finder(filter: Filter.byKey(post.id)); + return await _postsStore.delete( + _db, + finder: finder, + ); + } + + Future deleteAll() async { + await _postsStore.drop( + _db, + ); + } + +} diff --git a/lib/data/network/apis/posts/post_api.dart b/lib/data/network/apis/posts/post_api.dart new file mode 100644 index 00000000..154e4ecd --- /dev/null +++ b/lib/data/network/apis/posts/post_api.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:boilerplate/data/network/constants/endpoints.dart'; +import 'package:boilerplate/data/network/dio_client.dart'; +import 'package:boilerplate/data/network/rest_client.dart'; +import 'package:boilerplate/models/post/post_list.dart'; + +class PostApi { + // dio instance + final DioClient _dioClient; + + // rest-client instance + final RestClient _restClient; + + // injecting dio instance + PostApi(this._dioClient, this._restClient); + + /// Returns list of post in response + Future getPosts() async { + try { + final res = await _dioClient.get(Endpoints.getPosts); + return PostList.fromJson(res); + } catch (e) { + print(e.toString()); + throw e; + } + } + +/// sample api call with default rest client +// Future getPosts() { +// +// return _restClient +// .get(Endpoints.getPosts) +// .then((dynamic res) => PostsList.fromJson(res)) +// .catchError((error) => throw NetworkException(message: error)); +// } + +} diff --git a/lib/data/network/constants/endpoints.dart b/lib/data/network/constants/endpoints.dart new file mode 100644 index 00000000..160227aa --- /dev/null +++ b/lib/data/network/constants/endpoints.dart @@ -0,0 +1,15 @@ +class Endpoints { + Endpoints._(); + + // base url + static const String baseUrl = "http://jsonplaceholder.typicode.com"; + + // receiveTimeout + static const int receiveTimeout = 15000; + + // connectTimeout + static const int connectionTimeout = 30000; + + // booking endpoints + static const String getPosts = baseUrl + "/posts"; +} \ No newline at end of file diff --git a/lib/data/network/dio_client.dart b/lib/data/network/dio_client.dart new file mode 100644 index 00000000..3509fd1c --- /dev/null +++ b/lib/data/network/dio_client.dart @@ -0,0 +1,108 @@ +import 'package:dio/dio.dart'; + +class DioClient { + // dio instance + final Dio _dio; + + // injecting dio instance + DioClient(this._dio); + + // Get:----------------------------------------------------------------------- + Future get( + String uri, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + try { + final Response response = await _dio.get( + uri, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } catch (e) { + print(e.toString()); + throw e; + } + } + + // Post:---------------------------------------------------------------------- + Future post( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + final Response response = await _dio.post( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } catch (e) { + throw e; + } + } + + // Put:----------------------------------------------------------------------- + Future put( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + final Response response = await _dio.put( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } catch (e) { + throw e; + } + } + + // Delete:-------------------------------------------------------------------- + Future delete( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + final Response response = await _dio.delete( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + return response.data; + } catch (e) { + throw e; + } + } +} diff --git a/lib/data/network/exceptions/network_exceptions.dart b/lib/data/network/exceptions/network_exceptions.dart new file mode 100644 index 00000000..8ba446d2 --- /dev/null +++ b/lib/data/network/exceptions/network_exceptions.dart @@ -0,0 +1,11 @@ +class NetworkException implements Exception { + String? message; + int? statusCode; + + NetworkException({this.message, this.statusCode}); +} + +class AuthException extends NetworkException { + AuthException({message, statusCode}) + : super(message: message, statusCode: statusCode); +} diff --git a/lib/data/network/rest_client.dart b/lib/data/network/rest_client.dart new file mode 100644 index 00000000..b5fd6e57 --- /dev/null +++ b/lib/data/network/rest_client.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:boilerplate/data/network/constants/endpoints.dart'; +import 'package:http/http.dart' as http; + +import 'exceptions/network_exceptions.dart'; + +class RestClient { + // instantiate json decoder for json serialization + final JsonDecoder _decoder = JsonDecoder(); + + // Get:----------------------------------------------------------------------- + Future get(String path) { + return http.get(Uri.https(Endpoints.baseUrl, path)).then(_createResponse); + } + + // Post:---------------------------------------------------------------------- + Future post(String path, + {Map? headers, body, encoding}) { + return http + .post( + Uri.https(Endpoints.baseUrl, path), + body: body, + headers: headers, + encoding: encoding, + ) + .then(_createResponse); + } + + // Put:---------------------------------------------------------------------- + Future put(String path, + {Map? headers, body, encoding}) { + return http + .put( + Uri.https(Endpoints.baseUrl, path), + body: body, + headers: headers, + encoding: encoding, + ) + .then(_createResponse); + } + + // Delete:---------------------------------------------------------------------- + Future delete(String path, + {Map? headers, body, encoding}) { + return http + .delete( + Uri.https(Endpoints.baseUrl, path), + body: body, + headers: headers, + encoding: encoding, + ) + .then(_createResponse); + } + + // Response:------------------------------------------------------------------ + dynamic _createResponse(http.Response response) { + final String res = response.body; + final int statusCode = response.statusCode; + + if (statusCode < 200 || statusCode > 400) { + throw NetworkException( + message: 'Error fetching data from server', statusCode: statusCode); + } + + return _decoder.convert(res); + } +} diff --git a/lib/data/repository.dart b/lib/data/repository.dart new file mode 100644 index 00000000..49b53580 --- /dev/null +++ b/lib/data/repository.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:boilerplate/data/local/datasources/post/post_datasource.dart'; +import 'package:boilerplate/data/sharedpref/shared_preference_helper.dart'; +import 'package:boilerplate/models/post/post.dart'; +import 'package:boilerplate/models/post/post_list.dart'; +import 'package:sembast/sembast.dart'; + +import 'local/constants/db_constants.dart'; +import 'network/apis/posts/post_api.dart'; + +class Repository { + // data source object + final PostDataSource _postDataSource; + + // api objects + final PostApi _postApi; + + // shared pref object + final SharedPreferenceHelper _sharedPrefsHelper; + + // constructor + Repository(this._postApi, this._sharedPrefsHelper, this._postDataSource); + + // Post: --------------------------------------------------------------------- + Future getPosts() async { + // check to see if posts are present in database, then fetch from database + // else make a network call to get all posts, store them into database for + // later use + return await _postApi.getPosts().then((postsList) { + postsList.posts?.forEach((post) { + _postDataSource.insert(post); + }); + + return postsList; + }).catchError((error) => throw error); + } + + Future> findPostById(int id) { + //creating filter + List filters = []; + + //check to see if dataLogsType is not null + Filter dataLogTypeFilter = Filter.equals(DBConstants.FIELD_ID, id); + filters.add(dataLogTypeFilter); + + //making db call + return _postDataSource + .getAllSortedByFilter(filters: filters) + .then((posts) => posts) + .catchError((error) => throw error); + } + + Future insert(Post post) => _postDataSource + .insert(post) + .then((id) => id) + .catchError((error) => throw error); + + Future update(Post post) => _postDataSource + .update(post) + .then((id) => id) + .catchError((error) => throw error); + + Future delete(Post post) => _postDataSource + .update(post) + .then((id) => id) + .catchError((error) => throw error); + + + // Login:--------------------------------------------------------------------- + Future login(String email, String password) async { + return await Future.delayed(Duration(seconds: 2), ()=> true); + } + + Future saveIsLoggedIn(bool value) => + _sharedPrefsHelper.saveIsLoggedIn(value); + + Future get isLoggedIn => _sharedPrefsHelper.isLoggedIn; + + // Theme: -------------------------------------------------------------------- + Future changeBrightnessToDark(bool value) => + _sharedPrefsHelper.changeBrightnessToDark(value); + + bool get isDarkMode => _sharedPrefsHelper.isDarkMode; + + // Language: ----------------------------------------------------------------- + Future changeLanguage(String value) => + _sharedPrefsHelper.changeLanguage(value); + + String? get currentLanguage => _sharedPrefsHelper.currentLanguage; +} \ No newline at end of file diff --git a/lib/data/sharedpref/constants/preferences.dart b/lib/data/sharedpref/constants/preferences.dart new file mode 100644 index 00000000..64fcc67b --- /dev/null +++ b/lib/data/sharedpref/constants/preferences.dart @@ -0,0 +1,8 @@ +class Preferences { + Preferences._(); + + static const String is_logged_in = "isLoggedIn"; + static const String auth_token = "authToken"; + static const String is_dark_mode = "is_dark_mode"; + static const String current_language = "current_language"; +} \ No newline at end of file diff --git a/lib/data/sharedpref/shared_preference_helper.dart b/lib/data/sharedpref/shared_preference_helper.dart new file mode 100644 index 00000000..446a9ddd --- /dev/null +++ b/lib/data/sharedpref/shared_preference_helper.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'constants/preferences.dart'; + +class SharedPreferenceHelper { + // shared pref instance + final SharedPreferences _sharedPreference; + + // constructor + SharedPreferenceHelper(this._sharedPreference); + + // General Methods: ---------------------------------------------------------- + Future get authToken async { + return _sharedPreference.getString(Preferences.auth_token); + } + + Future saveAuthToken(String authToken) async { + return _sharedPreference.setString(Preferences.auth_token, authToken); + } + + Future removeAuthToken() async { + return _sharedPreference.remove(Preferences.auth_token); + } + + // Login:--------------------------------------------------------------------- + Future get isLoggedIn async { + return _sharedPreference.getBool(Preferences.is_logged_in) ?? false; + } + + Future saveIsLoggedIn(bool value) async { + return _sharedPreference.setBool(Preferences.is_logged_in, value); + } + + // Theme:------------------------------------------------------ + bool get isDarkMode { + return _sharedPreference.getBool(Preferences.is_dark_mode) ?? false; + } + + Future changeBrightnessToDark(bool value) { + return _sharedPreference.setBool(Preferences.is_dark_mode, value); + } + + // Language:--------------------------------------------------- + String? get currentLanguage { + return _sharedPreference.getString(Preferences.current_language); + } + + Future changeLanguage(String language) { + return _sharedPreference.setString(Preferences.current_language, language); + } +} \ No newline at end of file diff --git a/lib/di/components/service_locator.dart b/lib/di/components/service_locator.dart new file mode 100644 index 00000000..c081e79c --- /dev/null +++ b/lib/di/components/service_locator.dart @@ -0,0 +1,56 @@ + +import 'package:boilerplate/data/local/datasources/post/post_datasource.dart'; +import 'package:boilerplate/data/network/apis/posts/post_api.dart'; +import 'package:boilerplate/data/network/dio_client.dart'; +import 'package:boilerplate/data/network/rest_client.dart'; +import 'package:boilerplate/data/repository.dart'; +import 'package:boilerplate/data/sharedpref/shared_preference_helper.dart'; +import 'package:boilerplate/di/module/local_module.dart'; +import 'package:boilerplate/di/module/network_module.dart'; +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:boilerplate/stores/form/form_store.dart'; +import 'package:boilerplate/stores/language/language_store.dart'; +import 'package:boilerplate/stores/post/post_store.dart'; +import 'package:boilerplate/stores/theme/theme_store.dart'; +import 'package:boilerplate/stores/user/user_store.dart'; +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:sembast/sembast.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final getIt = GetIt.instance; + +Future setupLocator() async { + // factories:----------------------------------------------------------------- + getIt.registerFactory(() => ErrorStore()); + getIt.registerFactory(() => FormStore()); + + // async singletons:---------------------------------------------------------- + getIt.registerSingletonAsync(() => LocalModule.provideDatabase()); + getIt.registerSingletonAsync(() => LocalModule.provideSharedPreferences()); + + // singletons:---------------------------------------------------------------- + getIt.registerSingleton(SharedPreferenceHelper(await getIt.getAsync())); + getIt.registerSingleton(NetworkModule.provideDio(getIt())); + getIt.registerSingleton(DioClient(getIt())); + getIt.registerSingleton(RestClient()); + + // api's:--------------------------------------------------------------------- + getIt.registerSingleton(PostApi(getIt(), getIt())); + + // data sources + getIt.registerSingleton(PostDataSource(await getIt.getAsync())); + + // repository:---------------------------------------------------------------- + getIt.registerSingleton(Repository( + getIt(), + getIt(), + getIt(), + )); + + // stores:-------------------------------------------------------------------- + getIt.registerSingleton(LanguageStore(getIt())); + getIt.registerSingleton(PostStore(getIt())); + getIt.registerSingleton(ThemeStore(getIt())); + getIt.registerSingleton(UserStore(getIt())); +} diff --git a/lib/di/module/local_module.dart b/lib/di/module/local_module.dart new file mode 100644 index 00000000..170884ae --- /dev/null +++ b/lib/di/module/local_module.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:boilerplate/data/local/constants/db_constants.dart'; +import 'package:boilerplate/utils/encryption/xxtea.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class LocalModule { + + + /// A singleton preference provider. + /// + /// Calling it multiple times will return the same instance. + static Future provideSharedPreferences() { + return SharedPreferences.getInstance(); + } + + /// A singleton database provider. + /// + /// Calling it multiple times will return the same instance. + static Future provideDatabase() async { + // Key for encryption + var encryptionKey = ""; + + // Get a platform-specific directory where persistent app data can be stored + final appDocumentDir = await getApplicationDocumentsDirectory(); + + // Path with the form: /platform-specific-directory/demo.db + final dbPath = join(appDocumentDir.path, DBConstants.DB_NAME); + + // Check to see if encryption is set, then provide codec + // else init normal db with path + var database; + if (encryptionKey.isNotEmpty) { + // Initialize the encryption codec with a user password + var codec = getXXTeaCodec(password: encryptionKey); + database = await databaseFactoryIo.openDatabase(dbPath, codec: codec); + } else { + database = await databaseFactoryIo.openDatabase(dbPath); + } + + // Return database instance + return database; + } +} diff --git a/lib/di/module/network_module.dart b/lib/di/module/network_module.dart new file mode 100644 index 00000000..f3fb079f --- /dev/null +++ b/lib/di/module/network_module.dart @@ -0,0 +1,43 @@ +import 'package:boilerplate/data/network/constants/endpoints.dart'; +import 'package:boilerplate/data/sharedpref/shared_preference_helper.dart'; +import 'package:dio/dio.dart'; + +abstract class NetworkModule { + /// A singleton dio provider. + /// + /// Calling it multiple times will return the same instance. + static Dio provideDio(SharedPreferenceHelper sharedPrefHelper) { + final dio = Dio(); + + dio + ..options.baseUrl = Endpoints.baseUrl + ..options.connectTimeout = Endpoints.connectionTimeout + ..options.receiveTimeout = Endpoints.receiveTimeout + ..options.headers = {'Content-Type': 'application/json; charset=utf-8'} + ..interceptors.add(LogInterceptor( + request: true, + responseBody: true, + requestBody: true, + requestHeader: true, + )) + ..interceptors.add( + InterceptorsWrapper( + onRequest: (RequestOptions options, + RequestInterceptorHandler handler) async { + // getting token + var token = await sharedPrefHelper.authToken; + + if (token != null) { + options.headers.putIfAbsent('Authorization', () => token); + } else { + print('Auth token is null'); + } + + return handler.next(options); + }, + ), + ); + + return dio; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 00000000..ef4ae22f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Invoke "debug painting" (press "p" in the console, choose the + // "Toggle Debug Paint" action from the Flutter Inspector in Android + // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) + // to see the wireframe for each widget. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/lib/models/language/Language.dart b/lib/models/language/Language.dart new file mode 100644 index 00000000..0c27ebaf --- /dev/null +++ b/lib/models/language/Language.dart @@ -0,0 +1,15 @@ +class Language { + /// the country code (IT,AF..) + String? code; + + /// the locale (en, es, da) + String? locale; + + /// the full name of language (English, Danish..) + String? language; + + /// map of keys used based on industry type (service worker, route etc) + Map? dictionary; + + Language({this.code, this.locale, this.language, this.dictionary}); +} diff --git a/lib/models/post/post.dart b/lib/models/post/post.dart new file mode 100644 index 00000000..449b6c23 --- /dev/null +++ b/lib/models/post/post.dart @@ -0,0 +1,28 @@ +class Post { + int? userId; + int? id; + String? title; + String? body; + + Post({ + this.userId, + this.id, + this.title, + this.body, + }); + + factory Post.fromMap(Map json) => Post( + userId: json["userId"], + id: json["id"], + title: json["title"], + body: json["body"], + ); + + Map toMap() => { + "userId": userId, + "id": id, + "title": title, + "body": body, + }; + +} diff --git a/lib/models/post/post_list.dart b/lib/models/post/post_list.dart new file mode 100644 index 00000000..e537a244 --- /dev/null +++ b/lib/models/post/post_list.dart @@ -0,0 +1,18 @@ +import 'package:boilerplate/models/post/post.dart'; + +class PostList { + final List? posts; + + PostList({ + this.posts, + }); + + factory PostList.fromJson(List json) { + List posts = []; + posts = json.map((post) => Post.fromMap(post)).toList(); + + return PostList( + posts: posts, + ); + } +} diff --git a/lib/stores/error/error_store.dart b/lib/stores/error/error_store.dart new file mode 100644 index 00000000..e98922fe --- /dev/null +++ b/lib/stores/error/error_store.dart @@ -0,0 +1,43 @@ +import 'package:mobx/mobx.dart'; + +part 'error_store.g.dart'; + +class ErrorStore = _ErrorStore with _$ErrorStore; + +abstract class _ErrorStore with Store { + + // disposers + late List _disposers; + + // constructor:--------------------------------------------------------------- + _ErrorStore() { + _disposers = [ + reaction((_) => errorMessage, reset, delay: 200), + ]; + } + + // store variables:----------------------------------------------------------- + @observable + String errorMessage = ''; + + + // actions:------------------------------------------------------------------- + @action + void setErrorMessage(String message) { + this.errorMessage = message; + } + + @action + void reset(String value) { + print('calling reset'); + errorMessage = ''; + } + + // dispose:------------------------------------------------------------------- + @action + dispose() { + for (final d in _disposers) { + d(); + } + } +} \ No newline at end of file diff --git a/lib/stores/error/error_store.g.dart b/lib/stores/error/error_store.g.dart new file mode 100644 index 00000000..5b1d960b --- /dev/null +++ b/lib/stores/error/error_store.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'error_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars + +mixin _$ErrorStore on _ErrorStore, Store { + final _$errorMessageAtom = Atom(name: '_ErrorStore.errorMessage'); + + @override + String get errorMessage { + _$errorMessageAtom.reportObserved(); + return super.errorMessage; + } + + @override + set errorMessage(String value) { + _$errorMessageAtom.context + .checkIfStateModificationsAreAllowed(_$errorMessageAtom); + super.errorMessage = value; + _$errorMessageAtom.reportChanged(); + } + + final _$showErrorAtom = Atom(name: '_ErrorStore.showError'); + + @override + bool get showError { + _$showErrorAtom.reportObserved(); + return super.showError; + } + + @override + set showError(bool value) { + _$showErrorAtom.context + .checkIfStateModificationsAreAllowed(_$showErrorAtom); + super.showError = value; + _$showErrorAtom.reportChanged(); + } +} diff --git a/lib/stores/form/form_store.dart b/lib/stores/form/form_store.dart new file mode 100644 index 00000000..6e2b6f4a --- /dev/null +++ b/lib/stores/form/form_store.dart @@ -0,0 +1,177 @@ +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:mobx/mobx.dart'; +import 'package:validators/validators.dart'; + +part 'form_store.g.dart'; + +class FormStore = _FormStore with _$FormStore; + +abstract class _FormStore with Store { + // store for handling form errors + final FormErrorStore formErrorStore = FormErrorStore(); + + // store for handling error messages + final ErrorStore errorStore = ErrorStore(); + + _FormStore() { + _setupValidations(); + } + + // disposers:----------------------------------------------------------------- + late List _disposers; + + void _setupValidations() { + _disposers = [ + reaction((_) => userEmail, validateUserEmail), + reaction((_) => password, validatePassword), + reaction((_) => confirmPassword, validateConfirmPassword) + ]; + } + + // store variables:----------------------------------------------------------- + @observable + String userEmail = ''; + + @observable + String password = ''; + + @observable + String confirmPassword = ''; + + @observable + bool success = false; + + @observable + bool loading = false; + + @computed + bool get canLogin => + !formErrorStore.hasErrorsInLogin && userEmail.isNotEmpty && password.isNotEmpty; + + @computed + bool get canRegister => + !formErrorStore.hasErrorsInRegister && + userEmail.isNotEmpty && + password.isNotEmpty && + confirmPassword.isNotEmpty; + + @computed + bool get canForgetPassword => + !formErrorStore.hasErrorInForgotPassword && userEmail.isNotEmpty; + + // actions:------------------------------------------------------------------- + @action + void setUserId(String value) { + userEmail = value; + } + + @action + void setPassword(String value) { + password = value; + } + + @action + void setConfirmPassword(String value) { + confirmPassword = value; + } + + @action + void validateUserEmail(String value) { + if (value.isEmpty) { + formErrorStore.userEmail = "Email can't be empty"; + } else if (!isEmail(value)) { + formErrorStore.userEmail = 'Please enter a valid email address'; + } else { + formErrorStore.userEmail = null; + } + } + + @action + void validatePassword(String value) { + if (value.isEmpty) { + formErrorStore.password = "Password can't be empty"; + } else if (value.length < 6) { + formErrorStore.password = "Password must be at-least 6 characters long"; + } else { + formErrorStore.password = null; + } + } + + @action + void validateConfirmPassword(String value) { + if (value.isEmpty) { + formErrorStore.confirmPassword = "Confirm password can't be empty"; + } else if (value != password) { + formErrorStore.confirmPassword = "Password doen't match"; + } else { + formErrorStore.confirmPassword = null; + } + } + + @action + Future register() async { + loading = true; + } + + @action + Future login() async { + loading = true; + + Future.delayed(Duration(milliseconds: 2000)).then((future) { + loading = false; + success = true; + }).catchError((e) { + loading = false; + success = false; + errorStore.errorMessage = e.toString().contains("ERROR_USER_NOT_FOUND") + ? "Username and password doesn't match" + : "Something went wrong, please check your internet connection and try again"; + print(e); + }); + } + + @action + Future forgotPassword() async { + loading = true; + } + + @action + Future logout() async { + loading = true; + } + + // general methods:----------------------------------------------------------- + void dispose() { + for (final d in _disposers) { + d(); + } + } + + void validateAll() { + validatePassword(password); + validateUserEmail(userEmail); + } +} + +class FormErrorStore = _FormErrorStore with _$FormErrorStore; + +abstract class _FormErrorStore with Store { + @observable + String? userEmail; + + @observable + String? password; + + @observable + String? confirmPassword; + + @computed + bool get hasErrorsInLogin => userEmail != null || password != null; + + @computed + bool get hasErrorsInRegister => + userEmail != null || password != null || confirmPassword != null; + + @computed + bool get hasErrorInForgotPassword => userEmail != null; +} diff --git a/lib/stores/form/form_store.g.dart b/lib/stores/form/form_store.g.dart new file mode 100644 index 00000000..2e079822 --- /dev/null +++ b/lib/stores/form/form_store.g.dart @@ -0,0 +1,281 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'form_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars + +mixin _$FormStore on _FormStore, Store { + Computed _$canLoginComputed; + + @override + bool get canLogin => + (_$canLoginComputed ??= Computed(() => super.canLogin)).value; + Computed _$canRegisterComputed; + + @override + bool get canRegister => + (_$canRegisterComputed ??= Computed(() => super.canRegister)).value; + Computed _$canForgetPasswordComputed; + + @override + bool get canForgetPassword => (_$canForgetPasswordComputed ??= + Computed(() => super.canForgetPassword)) + .value; + + final _$postsListAtom = Atom(name: '_FormStore.postsList'); + + @override + PostsList get postsList { + _$postsListAtom.reportObserved(); + return super.postsList; + } + + @override + set postsList(PostsList value) { + _$postsListAtom.context + .checkIfStateModificationsAreAllowed(_$postsListAtom); + super.postsList = value; + _$postsListAtom.reportChanged(); + } + + final _$userEmailAtom = Atom(name: '_FormStore.userEmail'); + + @override + String get userEmail { + _$userEmailAtom.reportObserved(); + return super.userEmail; + } + + @override + set userEmail(String value) { + _$userEmailAtom.context + .checkIfStateModificationsAreAllowed(_$userEmailAtom); + super.userEmail = value; + _$userEmailAtom.reportChanged(); + } + + final _$passwordAtom = Atom(name: '_FormStore.password'); + + @override + String get password { + _$passwordAtom.reportObserved(); + return super.password; + } + + @override + set password(String value) { + _$passwordAtom.context.checkIfStateModificationsAreAllowed(_$passwordAtom); + super.password = value; + _$passwordAtom.reportChanged(); + } + + final _$confirmPasswordAtom = Atom(name: '_FormStore.confirmPassword'); + + @override + String get confirmPassword { + _$confirmPasswordAtom.reportObserved(); + return super.confirmPassword; + } + + @override + set confirmPassword(String value) { + _$confirmPasswordAtom.context + .checkIfStateModificationsAreAllowed(_$confirmPasswordAtom); + super.confirmPassword = value; + _$confirmPasswordAtom.reportChanged(); + } + + final _$successAtom = Atom(name: '_FormStore.success'); + + @override + bool get success { + _$successAtom.reportObserved(); + return super.success; + } + + @override + set success(bool value) { + _$successAtom.context.checkIfStateModificationsAreAllowed(_$successAtom); + super.success = value; + _$successAtom.reportChanged(); + } + + final _$loadingAtom = Atom(name: '_FormStore.loading'); + + @override + bool get loading { + _$loadingAtom.reportObserved(); + return super.loading; + } + + @override + set loading(bool value) { + _$loadingAtom.context.checkIfStateModificationsAreAllowed(_$loadingAtom); + super.loading = value; + _$loadingAtom.reportChanged(); + } + + final _$registerAsyncAction = AsyncAction('register'); + + @override + Future register() { + return _$registerAsyncAction.run(() => super.register()); + } + + final _$loginAsyncAction = AsyncAction('login'); + + @override + Future login() { + return _$loginAsyncAction.run(() => super.login()); + } + + final _$forgotPasswordAsyncAction = AsyncAction('forgotPassword'); + + @override + Future forgotPassword() { + return _$forgotPasswordAsyncAction.run(() => super.forgotPassword()); + } + + final _$logoutAsyncAction = AsyncAction('logout'); + + @override + Future logout() { + return _$logoutAsyncAction.run(() => super.logout()); + } + + final _$_FormStoreActionController = ActionController(name: '_FormStore'); + + @override + void setUserId(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.setUserId(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } + + @override + void setPassword(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.setPassword(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } + + @override + void setConfirmPassword(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.setConfirmPassword(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } + + @override + void validateUserEmail(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.validateUserEmail(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } + + @override + void validatePassword(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.validatePassword(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } + + @override + void validateConfirmPassword(String value) { + final _$actionInfo = _$_FormStoreActionController.startAction(); + try { + return super.validateConfirmPassword(value); + } finally { + _$_FormStoreActionController.endAction(_$actionInfo); + } + } +} + +// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars + +mixin _$FormErrorStore on _FormErrorStore, Store { + Computed _$hasErrorsInLoginComputed; + + @override + bool get hasErrorsInLogin => (_$hasErrorsInLoginComputed ??= + Computed(() => super.hasErrorsInLogin)) + .value; + Computed _$hasErrorsInRegisterComputed; + + @override + bool get hasErrorsInRegister => (_$hasErrorsInRegisterComputed ??= + Computed(() => super.hasErrorsInRegister)) + .value; + Computed _$hasErrorInForgotPasswordComputed; + + @override + bool get hasErrorInForgotPassword => (_$hasErrorInForgotPasswordComputed ??= + Computed(() => super.hasErrorInForgotPassword)) + .value; + + final _$userEmailAtom = Atom(name: '_FormErrorStore.userEmail'); + + @override + String get userEmail { + _$userEmailAtom.reportObserved(); + return super.userEmail; + } + + @override + set userEmail(String value) { + _$userEmailAtom.context + .checkIfStateModificationsAreAllowed(_$userEmailAtom); + super.userEmail = value; + _$userEmailAtom.reportChanged(); + } + + final _$passwordAtom = Atom(name: '_FormErrorStore.password'); + + @override + String get password { + _$passwordAtom.reportObserved(); + return super.password; + } + + @override + set password(String value) { + _$passwordAtom.context.checkIfStateModificationsAreAllowed(_$passwordAtom); + super.password = value; + _$passwordAtom.reportChanged(); + } + + final _$confirmPasswordAtom = Atom(name: '_FormErrorStore.confirmPassword'); + + @override + String get confirmPassword { + _$confirmPasswordAtom.reportObserved(); + return super.confirmPassword; + } + + @override + set confirmPassword(String value) { + _$confirmPasswordAtom.context + .checkIfStateModificationsAreAllowed(_$confirmPasswordAtom); + super.confirmPassword = value; + _$confirmPasswordAtom.reportChanged(); + } +} diff --git a/lib/stores/language/language_store.dart b/lib/stores/language/language_store.dart new file mode 100644 index 00000000..1bcc9d6d --- /dev/null +++ b/lib/stores/language/language_store.dart @@ -0,0 +1,81 @@ +import 'package:boilerplate/data/repository.dart'; +import 'package:boilerplate/models/language/Language.dart'; +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:mobx/mobx.dart'; + +part 'language_store.g.dart'; + +class LanguageStore = _LanguageStore with _$LanguageStore; + +abstract class _LanguageStore with Store { + static const String TAG = "LanguageStore"; + + // repository instance + final Repository _repository; + + // store for handling errors + final ErrorStore errorStore = ErrorStore(); + + // supported languages + List supportedLanguages = [ + Language(code: 'US', locale: 'en', language: 'English'), + Language(code: 'DK', locale: 'da', language: 'Danish'), + Language(code: 'ES', locale: 'es', language: 'España'), + ]; + + // constructor:--------------------------------------------------------------- + _LanguageStore(Repository repository) + : this._repository = repository { + init(); + } + + // store variables:----------------------------------------------------------- + @observable + String _locale = "en"; + + @computed + String get locale => _locale; + + // actions:------------------------------------------------------------------- + @action + void changeLanguage(String value) { + _locale = value; + _repository.changeLanguage(value).then((_) { + // write additional logic here + }); + } + + @action + String getCode() { + var code; + + if (_locale == 'en') { + code = "US"; + } else if (_locale == 'da') { + code = "DK"; + } else if (_locale == 'es') { + code = "ES"; + } + + return code; + } + + @action + String? getLanguage() { + return supportedLanguages[supportedLanguages + .indexWhere((language) => language.locale == _locale)] + .language; + } + + // general:------------------------------------------------------------------- + void init() async { + // getting current language from shared preference + if(_repository.currentLanguage != null) { + _locale = _repository.currentLanguage!; + } + } + + // dispose:------------------------------------------------------------------- + @override + dispose() {} +} diff --git a/lib/stores/post/post_store.dart b/lib/stores/post/post_store.dart new file mode 100644 index 00000000..463d846d --- /dev/null +++ b/lib/stores/post/post_store.dart @@ -0,0 +1,50 @@ +import 'package:boilerplate/data/repository.dart'; +import 'package:boilerplate/models/post/post_list.dart'; +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:boilerplate/utils/dio/dio_error_util.dart'; +import 'package:mobx/mobx.dart'; + +part 'post_store.g.dart'; + +class PostStore = _PostStore with _$PostStore; + +abstract class _PostStore with Store { + // repository instance + late Repository _repository; + + // store for handling errors + final ErrorStore errorStore = ErrorStore(); + + // constructor:--------------------------------------------------------------- + _PostStore(Repository repository) : this._repository = repository; + + // store variables:----------------------------------------------------------- + static ObservableFuture emptyPostResponse = + ObservableFuture.value(null); + + @observable + ObservableFuture fetchPostsFuture = + ObservableFuture(emptyPostResponse); + + @observable + PostList? postList; + + @observable + bool success = false; + + @computed + bool get loading => fetchPostsFuture.status == FutureStatus.pending; + + // actions:------------------------------------------------------------------- + @action + Future getPosts() async { + final future = _repository.getPosts(); + fetchPostsFuture = ObservableFuture(future); + + future.then((postList) { + this.postList = postList; + }).catchError((error) { + errorStore.errorMessage = DioErrorUtil.handleError(error); + }); + } +} diff --git a/lib/stores/post/post_store.g.dart b/lib/stores/post/post_store.g.dart new file mode 100644 index 00000000..425891d2 --- /dev/null +++ b/lib/stores/post/post_store.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'post_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars + +mixin _$PostStore on _PostStore, Store { + final _$postsListAtom = Atom(name: '_PostStore.postsList'); + + @override + PostsList get postsList { + _$postsListAtom.reportObserved(); + return super.postsList; + } + + @override + set postsList(PostsList value) { + _$postsListAtom.context + .checkIfStateModificationsAreAllowed(_$postsListAtom); + super.postsList = value; + _$postsListAtom.reportChanged(); + } + + final _$successAtom = Atom(name: '_PostStore.success'); + + @override + bool get success { + _$successAtom.reportObserved(); + return super.success; + } + + @override + set success(bool value) { + _$successAtom.context.checkIfStateModificationsAreAllowed(_$successAtom); + super.success = value; + _$successAtom.reportChanged(); + } + + final _$loadingAtom = Atom(name: '_PostStore.loading'); + + @override + bool get loading { + _$loadingAtom.reportObserved(); + return super.loading; + } + + @override + set loading(bool value) { + _$loadingAtom.context.checkIfStateModificationsAreAllowed(_$loadingAtom); + super.loading = value; + _$loadingAtom.reportChanged(); + } + + final _$getPostsAsyncAction = AsyncAction('getPosts'); + + @override + Future getPosts() { + return _$getPostsAsyncAction.run(() => super.getPosts()); + } +} diff --git a/lib/stores/theme/theme_store.dart b/lib/stores/theme/theme_store.dart new file mode 100644 index 00000000..0f908742 --- /dev/null +++ b/lib/stores/theme/theme_store.dart @@ -0,0 +1,53 @@ +import 'package:boilerplate/data/repository.dart'; +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:flutter/material.dart'; +import 'package:mobx/mobx.dart'; + +part 'theme_store.g.dart'; + +class ThemeStore = _ThemeStore with _$ThemeStore; + +abstract class _ThemeStore with Store { + final String TAG = "_ThemeStore"; + + // repository instance + final Repository _repository; + + // store for handling errors + final ErrorStore errorStore = ErrorStore(); + + // store variables:----------------------------------------------------------- + @observable + bool _darkMode = false; + + + // getters:------------------------------------------------------------------- + bool get darkMode => _darkMode; + + // constructor:--------------------------------------------------------------- + _ThemeStore(Repository repository) + : this._repository = repository { + init(); + } + + // actions:------------------------------------------------------------------- + @action + Future changeBrightnessToDark(bool value) async { + _darkMode = value; + await _repository.changeBrightnessToDark(value); + } + + // general methods:----------------------------------------------------------- + Future init() async { + _darkMode = _repository.isDarkMode; + } + + bool isPlatformDark(BuildContext context) => + MediaQuery.platformBrightnessOf(context) == Brightness.dark; + + // dispose:------------------------------------------------------------------- + @override + dispose() { + + } +} diff --git a/lib/stores/user/user_store.dart b/lib/stores/user/user_store.dart new file mode 100644 index 00000000..cd419712 --- /dev/null +++ b/lib/stores/user/user_store.dart @@ -0,0 +1,92 @@ +import 'package:boilerplate/stores/error/error_store.dart'; +import 'package:mobx/mobx.dart'; + +import '../../data/repository.dart'; +import '../form/form_store.dart'; + +part 'user_store.g.dart'; + +class UserStore = _UserStore with _$UserStore; + +abstract class _UserStore with Store { + // repository instance + final Repository _repository; + + // store for handling form errors + final FormErrorStore formErrorStore = FormErrorStore(); + + // store for handling error messages + final ErrorStore errorStore = ErrorStore(); + + // bool to check if current user is logged in + bool isLoggedIn = false; + + // constructor:--------------------------------------------------------------- + _UserStore(Repository repository) : this._repository = repository { + + // setting up disposers + _setupDisposers(); + + // checking if user is logged in + repository.isLoggedIn.then((value) { + this.isLoggedIn = value; + }); + } + + // disposers:----------------------------------------------------------------- + late List _disposers; + + void _setupDisposers() { + _disposers = [ + reaction((_) => success, (_) => success = false, delay: 200), + ]; + } + + // empty responses:----------------------------------------------------------- + static ObservableFuture emptyLoginResponse = + ObservableFuture.value(false); + + // store variables:----------------------------------------------------------- + @observable + bool success = false; + + @observable + ObservableFuture loginFuture = emptyLoginResponse; + + @computed + bool get isLoading => loginFuture.status == FutureStatus.pending; + + // actions:------------------------------------------------------------------- + @action + Future login(String email, String password) async { + + final future = _repository.login(email, password); + loginFuture = ObservableFuture(future); + await future.then((value) async { + if (value) { + _repository.saveIsLoggedIn(true); + this.isLoggedIn = true; + this.success = true; + } else { + print('failed to login'); + } + }).catchError((e) { + print(e); + this.isLoggedIn = false; + this.success = false; + throw e; + }); + } + + logout() { + this.isLoggedIn = false; + _repository.saveIsLoggedIn(false); + } + + // general methods:----------------------------------------------------------- + void dispose() { + for (final d in _disposers) { + d(); + } + } +} \ No newline at end of file diff --git a/lib/ui/home/home.dart b/lib/ui/home/home.dart new file mode 100644 index 00000000..1c5eb600 --- /dev/null +++ b/lib/ui/home/home.dart @@ -0,0 +1,249 @@ +import 'package:another_flushbar/flushbar_helper.dart'; +import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; +import 'package:boilerplate/utils/routes/routes.dart'; +import 'package:boilerplate/stores/language/language_store.dart'; +import 'package:boilerplate/stores/post/post_store.dart'; +import 'package:boilerplate/stores/theme/theme_store.dart'; +import 'package:boilerplate/utils/locale/app_localization.dart'; +import 'package:boilerplate/widgets/progress_indicator_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:material_dialog/material_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class HomeScreen extends StatefulWidget { + @override + _HomeScreenState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + //stores:--------------------------------------------------------------------- + late PostStore _postStore; + late ThemeStore _themeStore; + late LanguageStore _languageStore; + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // initializing stores + _languageStore = Provider.of(context); + _themeStore = Provider.of(context); + _postStore = Provider.of(context); + + // check to see if already called api + if (!_postStore.loading) { + _postStore.getPosts(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: _buildAppBar(), + body: _buildBody(), + ); + } + + // app bar methods:----------------------------------------------------------- + PreferredSizeWidget _buildAppBar() { + return AppBar( + title: Text(AppLocalizations.of(context).translate('home_tv_posts')), + actions: _buildActions(context), + ); + } + + List _buildActions(BuildContext context) { + return [ + _buildLanguageButton(), + _buildThemeButton(), + _buildLogoutButton(), + ]; + } + + Widget _buildThemeButton() { + return Observer( + builder: (context) { + return IconButton( + onPressed: () { + _themeStore.changeBrightnessToDark(!_themeStore.darkMode); + }, + icon: Icon( + _themeStore.darkMode ? Icons.brightness_5 : Icons.brightness_3, + ), + ); + }, + ); + } + + Widget _buildLogoutButton() { + return IconButton( + onPressed: () { + SharedPreferences.getInstance().then((preference) { + preference.setBool(Preferences.is_logged_in, false); + Navigator.of(context).pushReplacementNamed(Routes.login); + }); + }, + icon: Icon( + Icons.power_settings_new, + ), + ); + } + + Widget _buildLanguageButton() { + return IconButton( + onPressed: () { + _buildLanguageDialog(); + }, + icon: Icon( + Icons.language, + ), + ); + } + + // body methods:-------------------------------------------------------------- + Widget _buildBody() { + return Stack( + children: [ + _handleErrorMessage(), + _buildMainContent(), + ], + ); + } + + Widget _buildMainContent() { + return Observer( + builder: (context) { + return _postStore.loading + ? CustomProgressIndicatorWidget() + : Material(child: _buildListView()); + }, + ); + } + + Widget _buildListView() { + return _postStore.postList != null + ? ListView.separated( + itemCount: _postStore.postList!.posts!.length, + separatorBuilder: (context, position) { + return Divider(); + }, + itemBuilder: (context, position) { + return _buildListItem(position); + }, + ) + : Center( + child: Text( + AppLocalizations.of(context).translate('home_tv_no_post_found'), + ), + ); + } + + Widget _buildListItem(int position) { + return ListTile( + dense: true, + leading: Icon(Icons.cloud_circle), + title: Text( + '${_postStore.postList?.posts?[position].title}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: Theme.of(context).textTheme.subtitle1, + ), + subtitle: Text( + '${_postStore.postList?.posts?[position].body}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ); + } + + Widget _handleErrorMessage() { + return Observer( + builder: (context) { + if (_postStore.errorStore.errorMessage.isNotEmpty) { + return _showErrorMessage(_postStore.errorStore.errorMessage); + } + + return SizedBox.shrink(); + }, + ); + } + + // General Methods:----------------------------------------------------------- + _showErrorMessage(String message) { + Future.delayed(Duration(milliseconds: 0), () { + if (message.isNotEmpty) { + FlushbarHelper.createError( + message: message, + title: AppLocalizations.of(context).translate('home_tv_error'), + duration: Duration(seconds: 3), + )..show(context); + } + }); + + return SizedBox.shrink(); + } + +_buildLanguageDialog() { + _showDialog( + context: context, + child: MaterialDialog( + borderRadius: 5.0, + enableFullWidth: true, + title: Text( + AppLocalizations.of(context).translate('home_tv_choose_language'), + style: TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + headerColor: Theme.of(context).primaryColor, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + closeButtonColor: Colors.white, + enableCloseButton: true, + enableBackButton: false, + onCloseButtonClicked: () { + Navigator.of(context).pop(); + }, + children: _languageStore.supportedLanguages + .map( + (object) => ListTile( + dense: true, + contentPadding: EdgeInsets.all(0.0), + title: Text( + object.language!, + style: TextStyle( + color: _languageStore.locale == object.locale + ? Theme.of(context).primaryColor + : _themeStore.darkMode ? Colors.white : Colors.black, + ), + ), + onTap: () { + Navigator.of(context).pop(); + // change user language based on selected locale + _languageStore.changeLanguage(object.locale!); + }, + ), + ) + .toList(), + ), + ); +} + + _showDialog({required BuildContext context, required Widget child}) { + showDialog( + context: context, + builder: (BuildContext context) => child, + ).then((T? value) { + // The value passed to Navigator.pop() or null. + }); + } +} diff --git a/lib/ui/login/login.dart b/lib/ui/login/login.dart new file mode 100644 index 00000000..22e37a1c --- /dev/null +++ b/lib/ui/login/login.dart @@ -0,0 +1,244 @@ +import 'package:another_flushbar/flushbar_helper.dart'; +import 'package:boilerplate/constants/assets.dart'; +import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; +import 'package:boilerplate/utils/routes/routes.dart'; +import 'package:boilerplate/stores/form/form_store.dart'; +import 'package:boilerplate/stores/theme/theme_store.dart'; +import 'package:boilerplate/utils/device/device_utils.dart'; +import 'package:boilerplate/utils/locale/app_localization.dart'; +import 'package:boilerplate/widgets/app_icon_widget.dart'; +import 'package:boilerplate/widgets/empty_app_bar_widget.dart'; +import 'package:boilerplate/widgets/progress_indicator_widget.dart'; +import 'package:boilerplate/widgets/rounded_button_widget.dart'; +import 'package:boilerplate/widgets/textfield_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LoginScreen extends StatefulWidget { + @override + _LoginScreenState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + //text controllers:----------------------------------------------------------- + TextEditingController _userEmailController = TextEditingController(); + TextEditingController _passwordController = TextEditingController(); + + //stores:--------------------------------------------------------------------- + late ThemeStore _themeStore; + + //focus node:----------------------------------------------------------------- + late FocusNode _passwordFocusNode; + + //stores:--------------------------------------------------------------------- + final _store = FormStore(); + + @override + void initState() { + super.initState(); + _passwordFocusNode = FocusNode(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _themeStore = Provider.of(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + primary: true, + appBar: EmptyAppBar(), + body: _buildBody(), + ); + } + + // body methods:-------------------------------------------------------------- + Widget _buildBody() { + return Material( + child: Stack( + children: [ + MediaQuery.of(context).orientation == Orientation.landscape + ? Row( + children: [ + Expanded( + flex: 1, + child: _buildLeftSide(), + ), + Expanded( + flex: 1, + child: _buildRightSide(), + ), + ], + ) + : Center(child: _buildRightSide()), + Observer( + builder: (context) { + return _store.success + ? navigate(context) + : _showErrorMessage(_store.errorStore.errorMessage); + }, + ), + Observer( + builder: (context) { + return Visibility( + visible: _store.loading, + child: CustomProgressIndicatorWidget(), + ); + }, + ) + ], + ), + ); + } + + Widget _buildLeftSide() { + return SizedBox.expand( + child: Image.asset( + Assets.carBackground, + fit: BoxFit.cover, + ), + ); + } + + Widget _buildRightSide() { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppIconWidget(image: 'assets/icons/ic_appicon.png'), + SizedBox(height: 24.0), + _buildUserIdField(), + _buildPasswordField(), + _buildForgotPasswordButton(), + _buildSignInButton() + ], + ), + ), + ); + } + + Widget _buildUserIdField() { + return Observer( + builder: (context) { + return TextFieldWidget( + hint: AppLocalizations.of(context).translate('login_et_user_email'), + inputType: TextInputType.emailAddress, + icon: Icons.person, + iconColor: _themeStore.darkMode ? Colors.white70 : Colors.black54, + textController: _userEmailController, + inputAction: TextInputAction.next, + autoFocus: false, + onChanged: (value) { + _store.setUserId(_userEmailController.text); + }, + onFieldSubmitted: (value) { + FocusScope.of(context).requestFocus(_passwordFocusNode); + }, + errorText: _store.formErrorStore.userEmail, + ); + }, + ); + } + + Widget _buildPasswordField() { + return Observer( + builder: (context) { + return TextFieldWidget( + hint: + AppLocalizations.of(context).translate('login_et_user_password'), + isObscure: true, + padding: EdgeInsets.only(top: 16.0), + icon: Icons.lock, + iconColor: _themeStore.darkMode ? Colors.white70 : Colors.black54, + textController: _passwordController, + focusNode: _passwordFocusNode, + errorText: _store.formErrorStore.password, + onChanged: (value) { + _store.setPassword(_passwordController.text); + }, + ); + }, + ); + } + + Widget _buildForgotPasswordButton() { + return Align( + alignment: FractionalOffset.centerRight, + child: FlatButton( + padding: EdgeInsets.all(0.0), + child: Text( + AppLocalizations.of(context).translate('login_btn_forgot_password'), + style: Theme.of(context) + .textTheme + .caption + ?.copyWith(color: Colors.orangeAccent), + ), + onPressed: () {}, + ), + ); + } + + Widget _buildSignInButton() { + return RoundedButtonWidget( + buttonText: AppLocalizations.of(context).translate('login_btn_sign_in'), + buttonColor: Colors.orangeAccent, + textColor: Colors.white, + onPressed: () async { + if (_store.canLogin) { + DeviceUtils.hideKeyboard(context); + _store.login(); + } else { + _showErrorMessage('Please fill in all fields'); + } + }, + ); + } + + Widget navigate(BuildContext context) { + SharedPreferences.getInstance().then((prefs) { + prefs.setBool(Preferences.is_logged_in, true); + }); + + Future.delayed(Duration(milliseconds: 0), () { + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.home, (Route route) => false); + }); + + return Container(); + } + + // General Methods:----------------------------------------------------------- + _showErrorMessage(String message) { + if (message.isNotEmpty) { + Future.delayed(Duration(milliseconds: 0), () { + if (message.isNotEmpty) { + FlushbarHelper.createError( + message: message, + title: AppLocalizations.of(context).translate('home_tv_error'), + duration: Duration(seconds: 3), + )..show(context); + } + }); + } + + return SizedBox.shrink(); + } + + // dispose:------------------------------------------------------------------- + @override + void dispose() { + // Clean up the controller when the Widget is removed from the Widget tree + _userEmailController.dispose(); + _passwordController.dispose(); + _passwordFocusNode.dispose(); + super.dispose(); + } +} diff --git a/lib/ui/my_app.dart b/lib/ui/my_app.dart new file mode 100644 index 00000000..b9d4f25b --- /dev/null +++ b/lib/ui/my_app.dart @@ -0,0 +1,65 @@ +import 'package:boilerplate/constants/app_theme.dart'; +import 'package:boilerplate/constants/strings.dart'; +import 'package:boilerplate/data/repository.dart'; +import 'package:boilerplate/di/components/service_locator.dart'; +import 'package:boilerplate/stores/language/language_store.dart'; +import 'package:boilerplate/stores/post/post_store.dart'; +import 'package:boilerplate/stores/theme/theme_store.dart'; +import 'package:boilerplate/stores/user/user_store.dart'; +import 'package:boilerplate/ui/home/home.dart'; +import 'package:boilerplate/ui/login/login.dart'; +import 'package:boilerplate/utils/locale/app_localization.dart'; +import 'package:boilerplate/utils/routes/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:provider/provider.dart'; + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + // Create your store as a final variable in a base Widget. This works better + // with Hot Reload than creating it directly in the `build` function. + final ThemeStore _themeStore = ThemeStore(getIt()); + final PostStore _postStore = PostStore(getIt()); + final LanguageStore _languageStore = LanguageStore(getIt()); + final UserStore _userStore = UserStore(getIt()); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider(create: (_) => _themeStore), + Provider(create: (_) => _postStore), + Provider(create: (_) => _languageStore), + ], + child: Observer( + name: 'global-observer', + builder: (context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: Strings.appName, + theme: _themeStore.darkMode + ? AppThemeData.darkThemeData + : AppThemeData.lightThemeData, + routes: Routes.routes, + locale: Locale(_languageStore.locale), + supportedLocales: _languageStore.supportedLanguages + .map((language) => Locale(language.locale!, language.code)) + .toList(), + localizationsDelegates: [ + // A class which loads the translations from JSON files + AppLocalizations.delegate, + // Built-in localization of basic text for Material widgets + GlobalMaterialLocalizations.delegate, + // Built-in localization for text direction LTR/RTL + GlobalWidgetsLocalizations.delegate, + // Built-in localization of basic text for Cupertino widgets + GlobalCupertinoLocalizations.delegate, + ], + home: _userStore.isLoggedIn ? HomeScreen() : LoginScreen(), + ); + }, + ), + ); + } +} diff --git a/lib/ui/splash/splash.dart b/lib/ui/splash/splash.dart new file mode 100644 index 00000000..4679e14b --- /dev/null +++ b/lib/ui/splash/splash.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import 'package:boilerplate/constants/assets.dart'; +import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; +import 'package:boilerplate/utils/routes/routes.dart'; +import 'package:boilerplate/widgets/app_icon_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SplashScreen extends StatefulWidget { + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + startTimer(); + } + + @override + Widget build(BuildContext context) { + + return Material( + child: Center(child: AppIconWidget(image: Assets.appLogo)), + ); + } + + startTimer() { + var _duration = Duration(milliseconds: 2000); + return Timer(_duration, navigate); + } + + navigate() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + + if (preferences.getBool(Preferences.is_logged_in) ?? false) { + Navigator.of(context).pushReplacementNamed(Routes.home); + } else { + Navigator.of(context).pushReplacementNamed(Routes.login); + } + } +} diff --git a/lib/utils/device/device_utils.dart b/lib/utils/device/device_utils.dart new file mode 100644 index 00000000..d3a58d1e --- /dev/null +++ b/lib/utils/device/device_utils.dart @@ -0,0 +1,38 @@ +// +import 'package:flutter/material.dart'; + +/// Helper class for device related operations. +/// +class DeviceUtils { + + /// + /// hides the keyboard if its already open + /// + static hideKeyboard(BuildContext context) { + FocusScope.of(context).unfocus(); + } + + /// + /// accepts a double [scale] and returns scaled sized based on the screen + /// orientation + /// + static double getScaledSize(BuildContext context, double scale) => + scale * + (MediaQuery.of(context).orientation == Orientation.portrait + ? MediaQuery.of(context).size.width + : MediaQuery.of(context).size.height); + + /// + /// accepts a double [scale] and returns scaled sized based on the screen + /// width + /// + static double getScaledWidth(BuildContext context, double scale) => + scale * MediaQuery.of(context).size.width; + + /// + /// accepts a double [scale] and returns scaled sized based on the screen + /// height + /// + static double getScaledHeight(BuildContext context, double scale) => + scale * MediaQuery.of(context).size.height; +} \ No newline at end of file diff --git a/lib/utils/dio/dio_error_util.dart b/lib/utils/dio/dio_error_util.dart new file mode 100644 index 00000000..fbd08812 --- /dev/null +++ b/lib/utils/dio/dio_error_util.dart @@ -0,0 +1,35 @@ +import 'package:dio/dio.dart'; + +class DioErrorUtil { + // general methods:------------------------------------------------------------ + static String handleError(DioError error) { + String errorDescription = ""; + if (error is DioError) { + switch (error.type) { + case DioErrorType.cancel: + errorDescription = "Request to API server was cancelled"; + break; + case DioErrorType.connectTimeout: + errorDescription = "Connection timeout with API server"; + break; + case DioErrorType.other: + errorDescription = + "Connection to API server failed due to internet connection"; + break; + case DioErrorType.receiveTimeout: + errorDescription = "Receive timeout in connection with API server"; + break; + case DioErrorType.response: + errorDescription = + "Received invalid status code: ${error.response?.statusCode}"; + break; + case DioErrorType.sendTimeout: + errorDescription = "Send timeout in connection with API server"; + break; + } + } else { + errorDescription = "Unexpected error occured"; + } + return errorDescription; + } +} \ No newline at end of file diff --git a/lib/utils/encryption/xxtea.dart b/lib/utils/encryption/xxtea.dart new file mode 100644 index 00000000..184e5858 --- /dev/null +++ b/lib/utils/encryption/xxtea.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; + +import 'package:sembast/sembast.dart'; +import 'package:xxtea/xxtea.dart'; + +class _XXTeaEncoder extends Converter, String> { + final String key; + + _XXTeaEncoder(this.key); + + @override + String convert(Map input) => + xxtea.encryptToString(json.encode(input), key)!; +} + +class _XXTeaDecoder extends Converter> { + final String key; + + _XXTeaDecoder(this.key); + + @override + Map convert(String input) { + var result = json.decode(xxtea.decryptToString(input, key)!); + if (result is Map) { + return result.cast(); + } + throw FormatException('invalid input $input'); + } +} + +/// Simple encryption codec using xxtea +/// It requires a password to encrypt/decrypt the data +class _XXTeaCodec extends Codec, String> { + late _XXTeaEncoder _encoder; + late _XXTeaDecoder _decoder; + + /// A non null [password] to use for the encryption/decryption + _XXTeaCodec(String password) { + _encoder = _XXTeaEncoder(password); + _decoder = _XXTeaDecoder(password); + } + + @override + Converter> get decoder => _decoder; + + @override + Converter, String> get encoder => _encoder; +} + +/// Create a codec to use when opening an encrypted sembast database +/// +/// The usage is then +/// +/// ```dart +/// // Initialize the encryption codec with a user password +/// var codec = getXXTeaCodec(password: '[your_user_password]'); +/// // Open the database with the codec +/// Database db = await factory.openDatabase(dbPath, codec: codec); +/// +/// // ...your database is ready to use as encrypted +/// ``` +SembastCodec getXXTeaCodec({required String password}) => + SembastCodec(signature: 'xxtea', codec: _XXTeaCodec(password)); diff --git a/lib/utils/locale/app_localization.dart b/lib/utils/locale/app_localization.dart new file mode 100644 index 00000000..1b4ea781 --- /dev/null +++ b/lib/utils/locale/app_localization.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AppLocalizations { + // localization variables + final Locale locale; + late Map localizedStrings; + + // Static member to have a simple access to the delegate from the MaterialApp + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + // constructor + AppLocalizations(this.locale); + + // Helper method to keep the code in the widgets concise + // Localizations are accessed using an InheritedWidget "of" syntax + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + // This is a helper method that will load local specific strings from file + // present in lang folder + Future load() async { + // Load the language JSON file from the "lang" folder + String jsonString = + await rootBundle.loadString('assets/lang/${locale.languageCode}.json'); + Map jsonMap = json.decode(jsonString); + + localizedStrings = jsonMap.map((key, value) { + return MapEntry( + key, value.toString().replaceAll(r"\'", "'").replaceAll(r"\t", " ")); + }); + + return true; + } + + // This method will be called from every widget which needs a localized text + String translate(String key) { + return localizedStrings[key]!; + } +} + +// LocalizationsDelegate is a factory for a set of localized resources +// In this case, the localized strings will be gotten in an AppLocalizations object +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + // ignore: non_constant_identifier_names + final String TAG = "AppLocalizations"; + + // This delegate instance will never change (it doesn't even have fields!) + // It can provide a constant constructor. + const _AppLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) { + // Include all of your supported language codes here + return ['en', 'es', 'da'].contains(locale.languageCode); + } + + @override + Future load(Locale locale) async { + // AppLocalizations class is where the JSON loading actually runs + AppLocalizations localizations = new AppLocalizations(locale); + await localizations.load(); + return localizations; + } + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} diff --git a/lib/utils/routes/routes.dart b/lib/utils/routes/routes.dart new file mode 100644 index 00000000..9533a07f --- /dev/null +++ b/lib/utils/routes/routes.dart @@ -0,0 +1,22 @@ +import 'package:boilerplate/ui/home/home.dart'; +import 'package:boilerplate/ui/login/login.dart'; +import 'package:boilerplate/ui/splash/splash.dart'; +import 'package:flutter/material.dart'; + +class Routes { + Routes._(); + + //static variables + static const String splash = '/splash'; + static const String login = '/login'; + static const String home = '/home'; + + static final routes = { + splash: (BuildContext context) => SplashScreen(), + login: (BuildContext context) => LoginScreen(), + home: (BuildContext context) => HomeScreen(), + }; +} + + + diff --git a/lib/widgets/app_icon_widget.dart b/lib/widgets/app_icon_widget.dart new file mode 100644 index 00000000..a4567c3b --- /dev/null +++ b/lib/widgets/app_icon_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class AppIconWidget extends StatelessWidget { + final image; + + const AppIconWidget({ + Key? key, + this.image, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + //getting screen size + var size = MediaQuery.of(context).size; + + //calculating container width + double imageSize; + if (MediaQuery.of(context).orientation == Orientation.portrait) { + imageSize = (size.width * 0.20); + } else { + imageSize = (size.height * 0.20); + } + + return Image.asset( + image, + height: imageSize, + ); + } +} diff --git a/lib/widgets/empty_app_bar_widget.dart b/lib/widgets/empty_app_bar_widget.dart new file mode 100644 index 00000000..7ea35f26 --- /dev/null +++ b/lib/widgets/empty_app_bar_widget.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget { + @override + Widget build(BuildContext context) { + return Container(); + } + + @override + Size get preferredSize => Size(0.0, 0.0); +} diff --git a/lib/widgets/progress_indicator_widget.dart b/lib/widgets/progress_indicator_widget.dart new file mode 100644 index 00000000..a11eeb5d --- /dev/null +++ b/lib/widgets/progress_indicator_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class CustomProgressIndicatorWidget extends StatelessWidget { + const CustomProgressIndicatorWidget({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: Container( + height: 100, + constraints: BoxConstraints.expand(), + child: FittedBox( + fit: BoxFit.none, + child: SizedBox( + height: 100, + width: 100, + child: Card( + child: Padding( + padding: const EdgeInsets.all(25.0), + child: CircularProgressIndicator(), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0)), + ), + ), + ), + decoration: BoxDecoration( + color: Color.fromARGB(100, 105, 105, 105)), + ), + ); + } +} diff --git a/lib/widgets/rounded_button_widget.dart b/lib/widgets/rounded_button_widget.dart new file mode 100644 index 00000000..2a68333b --- /dev/null +++ b/lib/widgets/rounded_button_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class RoundedButtonWidget extends StatelessWidget { + final String buttonText; + final Color buttonColor; + final Color textColor; + final VoidCallback onPressed; + + const RoundedButtonWidget({ + Key? key, + required this.buttonText, + required this.buttonColor, + this.textColor = Colors.white, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlatButton( + color: buttonColor, + shape: StadiumBorder(), + onPressed: onPressed, + child: Text( + buttonText, + style: Theme.of(context).textTheme.button!.copyWith(color: textColor), + ), + ); + } +} diff --git a/lib/widgets/textfield_widget.dart b/lib/widgets/textfield_widget.dart new file mode 100644 index 00000000..7b7fd611 --- /dev/null +++ b/lib/widgets/textfield_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class TextFieldWidget extends StatelessWidget { + final IconData icon; + final String? hint; + final String? errorText; + final bool isObscure; + final bool isIcon; + final TextInputType? inputType; + final TextEditingController textController; + final EdgeInsets padding; + final Color hintColor; + final Color iconColor; + final FocusNode? focusNode; + final ValueChanged? onFieldSubmitted; + final ValueChanged? onChanged; + final bool autoFocus; + final TextInputAction? inputAction; + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: TextFormField( + controller: textController, + focusNode: focusNode, + onFieldSubmitted: onFieldSubmitted, + onChanged: onChanged, + autofocus: autoFocus, + textInputAction: inputAction, + obscureText: this.isObscure, + maxLength: 25, + keyboardType: this.inputType, + style: Theme.of(context).textTheme.bodyText1, + decoration: InputDecoration( + hintText: this.hint, + hintStyle: + Theme.of(context).textTheme.bodyText1!.copyWith(color: hintColor), + errorText: errorText, + counterText: '', + icon: this.isIcon ? Icon(this.icon, color: iconColor) : null), + ), + ); + } + + const TextFieldWidget({ + Key? key, + required this.icon, + required this.errorText, + required this.textController, + this.inputType, + this.hint, + this.isObscure = false, + this.isIcon = true, + this.padding = const EdgeInsets.all(0), + this.hintColor = Colors.grey, + this.iconColor = Colors.grey, + this.focusNode, + this.onFieldSubmitted, + this.onChanged, + this.autoFocus = false, + this.inputAction, + }) : super(key: key); + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..0f3a736a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,160 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" +sdks: + dart: ">=2.18.6 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..b94bccd3 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,91 @@ +name: smart_insti +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.18.6 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/smart_insti.iml b/smart_insti.iml new file mode 100644 index 00000000..3ae446d1 --- /dev/null +++ b/smart_insti.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 00000000..5edd422f --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:smart_insti/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..c1530cc2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + smart_insti + + + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 00000000..c7c2ba38 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "smart_insti", + "short_name": "smart_insti", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 00000000..d05f1bd5 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(smart_insti LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "smart_insti") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..3f71e173 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..8b6d4680 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..b93c4c30 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..c832c4b1 --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 00000000..2520332d --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "smart_insti" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "smart_insti" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "smart_insti.exe" "\0" + VALUE "ProductName", "smart_insti" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..3a11b51d --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 00000000..28c23839 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 00000000..f34ae488 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"smart_insti", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 00000000..ddc7f3ef --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..157e871f --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 00000000..92ed5472 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 00000000..3f0e05cb --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 00000000..97f4439c --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 00000000..d9bcac1b --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_