Scalar Presence: brand-panel login (light/dark), Poppins, splash and launcher icons, fixed API base via dart-define, keep-signed-in, Android project
- Login reworked to the Brand panel layout with a dark-theme switch; 'Scalar Presence' two-line wordmark - Android project now tracked (was gitignored); pins shared_preferences_android 2.4.27 (2.5.0 crashed with a black screen) - API base URL fixed per build (--dart-define=API_BASE_URL), no in-app server setting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -7,7 +7,6 @@ build/
|
||||
.idea/
|
||||
.DS_Store
|
||||
ios/
|
||||
android/
|
||||
web/
|
||||
macos/
|
||||
linux/
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "84fc5cbb223bc12f83d65b647ff8a56caf779ffd"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
|
||||
base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
|
||||
- platform: android
|
||||
create_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
|
||||
base_revision: 84fc5cbb223bc12f83d65b647ff8a56caf779ffd
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.fistinnovations.scalar_attendance"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.fistinnovations.scalar_attendance"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,54 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:label="Scalar Presence"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.fistinnovations.scalar_attendance
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This newDsl flag was added by the Flutter template
|
||||
android.newDsl=false
|
||||
# This builtInKotlin flag was added by the Flutter template
|
||||
android.builtInKotlin=false
|
||||
@@ -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-9.1.0-all.zip
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -111,7 +111,7 @@ class ApiService {
|
||||
ApiService(this.settings);
|
||||
|
||||
Uri _uri(String p, [Map<String, String>? q]) =>
|
||||
Uri.parse('${settings.baseUrl}$p').replace(queryParameters: q);
|
||||
Uri.parse('${Settings.baseUrl}$p').replace(queryParameters: q);
|
||||
|
||||
Map<String, String> get _h => {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'api_service.dart';
|
||||
import 'settings.dart';
|
||||
import 'theme.dart';
|
||||
import 'screens/auth_shell.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
|
||||
@@ -9,6 +10,7 @@ Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final settings = Settings();
|
||||
await settings.load();
|
||||
if (settings.isLoggedIn && !await KeepSignedIn.wanted()) await settings.clearSession();
|
||||
runApp(ScalarEssApp(settings: settings));
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ class ScalarEssApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final api = ApiService(settings);
|
||||
return MaterialApp(
|
||||
title: 'Scalar360 Attendance',
|
||||
title: 'Scalar Presence',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildTheme(),
|
||||
home: settings.isLoggedIn
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// True for test builds pointed at the demo server (shows a "Demo" badge on the sign-in screen).
|
||||
const _apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
||||
final bool kDemoBuild = _apiBaseUrl.isNotEmpty && !_apiBaseUrl.contains('api.scalar360.com');
|
||||
|
||||
/// Light and dark colours for the sign-in screens (Scalar360 brand tokens).
|
||||
class AuthPalette {
|
||||
final bool dark;
|
||||
final Color page, surface, text, title, muted, line, border, accent, link, danger, fieldFill;
|
||||
const AuthPalette._({
|
||||
required this.dark,
|
||||
required this.page,
|
||||
required this.surface,
|
||||
required this.text,
|
||||
required this.title,
|
||||
required this.muted,
|
||||
required this.line,
|
||||
required this.border,
|
||||
required this.accent,
|
||||
required this.link,
|
||||
required this.danger,
|
||||
required this.fieldFill,
|
||||
});
|
||||
|
||||
static const light = AuthPalette._(
|
||||
dark: false,
|
||||
page: Color(0xFFFFFFFF),
|
||||
surface: Color(0xFFFFFFFF),
|
||||
text: Color(0xFF1B1B1B),
|
||||
title: Color(0xFF0B2545),
|
||||
muted: Color(0xFF616A75),
|
||||
line: Color(0xFF8A8A8A),
|
||||
border: Color(0xFFD5DCE5),
|
||||
accent: Color(0xFF00965B),
|
||||
link: Color(0xFF00965B),
|
||||
danger: Color(0xFFC62828),
|
||||
fieldFill: Color(0xFFF7F9FB),
|
||||
);
|
||||
|
||||
static const darkPalette = AuthPalette._(
|
||||
dark: true,
|
||||
page: Color(0xFF0E141B),
|
||||
surface: Color(0xFF151D26),
|
||||
text: Color(0xFFE8EDF2),
|
||||
title: Color(0xFFF1F5F9),
|
||||
muted: Color(0xFF9AA6B2),
|
||||
line: Color(0xFF5B6875),
|
||||
border: Color(0xFF2A3542),
|
||||
accent: Color(0xFF00BE73),
|
||||
link: Color(0xFF34D399),
|
||||
danger: Color(0xFFF87171),
|
||||
fieldFill: Color(0xFF1B2530),
|
||||
);
|
||||
}
|
||||
|
||||
/// Kept for screens that used the old names.
|
||||
class AuthColors {
|
||||
static const emerald = Color(0xFF00BE73);
|
||||
static const navy = Color(0xFF0B2545);
|
||||
static const muted = Color(0xFF616A75);
|
||||
}
|
||||
|
||||
/// The palette for the current sign-in screen.
|
||||
class AuthScope extends InheritedWidget {
|
||||
final AuthPalette palette;
|
||||
const AuthScope({super.key, required this.palette, required super.child});
|
||||
|
||||
static AuthScope of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<AuthScope>()!;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(AuthScope old) => old.palette != palette;
|
||||
}
|
||||
|
||||
/// Light / dark choice for the sign-in screens, remembered on the device ("system" until the user picks).
|
||||
class AuthThemeMode {
|
||||
static const _key = 'scalar360.auth.theme';
|
||||
static final notifier = ValueNotifier<ThemeMode>(ThemeMode.system);
|
||||
static bool _loaded = false;
|
||||
|
||||
static Future<void> load() async {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
final v = (await SharedPreferences.getInstance()).getString(_key);
|
||||
notifier.value = v == 'dark' ? ThemeMode.dark : v == 'light' ? ThemeMode.light : ThemeMode.system;
|
||||
}
|
||||
|
||||
static Future<void> set(ThemeMode mode) async {
|
||||
notifier.value = mode;
|
||||
await (await SharedPreferences.getInstance()).setString(_key, mode == ThemeMode.dark ? 'dark' : 'light');
|
||||
}
|
||||
}
|
||||
|
||||
/// "Keep me signed in": when it was off at sign-in, the saved session is dropped at the next app start.
|
||||
class KeepSignedIn {
|
||||
static const _key = 'scalar360.auth.keep_signed_in';
|
||||
static Future<void> save(bool keep) async => (await SharedPreferences.getInstance()).setBool(_key, keep);
|
||||
static Future<bool> wanted() async => (await SharedPreferences.getInstance()).getBool(_key) ?? true;
|
||||
}
|
||||
|
||||
/// Sign-in screen: a brand panel (logo, app name, tagline) above a rounded sheet with the form; light or dark.
|
||||
class AuthShell extends StatefulWidget {
|
||||
final String appName;
|
||||
final String title;
|
||||
/// Second line of the brand title under "Scalar360", e.g. "Presence" or "Point of Sale".
|
||||
final String product;
|
||||
/// One line about the app, shown on the brand panel.
|
||||
final String tagline;
|
||||
final List<Widget> children;
|
||||
|
||||
const AuthShell({
|
||||
super.key,
|
||||
required this.appName,
|
||||
required this.product,
|
||||
this.title = 'Sign in',
|
||||
this.tagline = '',
|
||||
required this.children,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AuthShell> createState() => _AuthShellState();
|
||||
}
|
||||
|
||||
class _AuthShellState extends State<AuthShell> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AuthThemeMode.load();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<ThemeMode>(
|
||||
valueListenable: AuthThemeMode.notifier,
|
||||
builder: (context, mode, _) {
|
||||
final media = MediaQuery.of(context);
|
||||
final dark = mode == ThemeMode.dark || (mode == ThemeMode.system && media.platformBrightness == Brightness.dark);
|
||||
final palette = dark ? AuthPalette.darkPalette : AuthPalette.light;
|
||||
return AuthScope(
|
||||
palette: palette,
|
||||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
// The brand panel is dark in both themes, so the status bar icons are always light.
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: palette.page,
|
||||
systemNavigationBarIconBrightness: dark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
child: MediaQuery(
|
||||
data: media.copyWith(textScaler: media.textScaler.clamp(minScaleFactor: 1, maxScaleFactor: 1.3)),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
brightness: dark ? Brightness.dark : Brightness.light,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: palette.accent,
|
||||
selectionColor: palette.accent.withValues(alpha: 0.3),
|
||||
selectionHandleColor: palette.accent,
|
||||
),
|
||||
),
|
||||
child: _PanelLayout(shell: widget, palette: palette),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ brand panel + sheet
|
||||
|
||||
class _PanelLayout extends StatelessWidget {
|
||||
final AuthShell shell;
|
||||
final AuthPalette palette;
|
||||
const _PanelLayout({required this.shell, required this.palette});
|
||||
|
||||
static const _gradient = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0B2545), Color(0xFF0B4A3C)],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = MediaQuery.of(context);
|
||||
final keyboard = media.viewInsets.bottom > 0;
|
||||
// Side by side on tablets and phones held sideways; stacked on phones held upright.
|
||||
final wide = media.size.width >= 720 || (media.size.width > media.size.height && media.size.width >= 560);
|
||||
final side = media.size.width < 360 ? 20.0 : 24.0;
|
||||
|
||||
Widget form({required EdgeInsets padding}) => _FillScroll(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_Title(shell.title),
|
||||
const SizedBox(height: 20),
|
||||
...shell.children,
|
||||
const Spacer(),
|
||||
const SizedBox(height: 20),
|
||||
const _Footer(padding: EdgeInsets.zero),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (wide) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0B2545),
|
||||
body: DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: _gradient),
|
||||
child: SafeArea(
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: _BrandPanel(shell: shell, compact: false, side: 32),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 16, 16),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: ColoredBox(
|
||||
color: palette.surface,
|
||||
child: form(padding: const EdgeInsets.fromLTRB(32, 32, 32, 20)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Upright phone: the panel (behind the status bar too) over a rounded sheet with the form.
|
||||
final panelBody = keyboard ? 64.0 : (media.size.height * 0.30).clamp(200.0, 270.0);
|
||||
return Scaffold(
|
||||
backgroundColor: palette.surface,
|
||||
body: Column(children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOut,
|
||||
height: media.padding.top + panelBody + 28,
|
||||
decoration: const BoxDecoration(gradient: _gradient),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: media.padding.top, bottom: 28),
|
||||
child: _BrandPanel(shell: shell, compact: keyboard, side: side),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Transform.translate(
|
||||
// the sheet's rounded top sits over the panel's lower edge
|
||||
offset: const Offset(0, -28),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: form(padding: EdgeInsets.fromLTRB(side, 28, side, 12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BrandPanel extends StatelessWidget {
|
||||
final AuthShell shell;
|
||||
final bool compact;
|
||||
final double side;
|
||||
const _BrandPanel({required this.shell, required this.compact, required this.side});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget knot(double h) =>
|
||||
Image.asset('assets/brand/scalar-knot.png', height: h, semanticLabel: 'Scalar360', filterQuality: FilterQuality.medium);
|
||||
|
||||
final content = compact
|
||||
? Row(children: [
|
||||
knot(34),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text('Scalar360 ${shell.product}',
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: _name(19, FontWeight.w600)),
|
||||
),
|
||||
])
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
knot(60),
|
||||
const SizedBox(height: 14),
|
||||
Text('Scalar360', style: _name(17, FontWeight.w500, opacity: 0.85)),
|
||||
Text(shell.product, style: _name(30, FontWeight.w700, height: 1.15)),
|
||||
if (shell.tagline.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(shell.tagline, style: const TextStyle(fontSize: 14, height: 1.45, color: Color(0xCCFFFFFF))),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
return Stack(children: [
|
||||
// soft glow, kept inside the panel
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(0.3, -0.3),
|
||||
child: Container(
|
||||
width: 240,
|
||||
height: 240,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(colors: [Color(0x5500BE73), Color(0x0000BE73)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(side, 0, side - 12, 0),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
if (!compact)
|
||||
Row(children: [
|
||||
if (kDemoBuild) const _DemoBadge(),
|
||||
const Spacer(),
|
||||
const _ThemeToggle(onDark: true),
|
||||
]),
|
||||
Expanded(
|
||||
child: compact
|
||||
? Row(children: [
|
||||
Expanded(child: content),
|
||||
const _ThemeToggle(onDark: true),
|
||||
])
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
// shrinks instead of overflowing on short screens
|
||||
child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: content),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
static TextStyle _name(double size, FontWeight weight, {double opacity = 1, double height = 1.25}) => TextStyle(
|
||||
fontSize: size,
|
||||
fontWeight: weight,
|
||||
height: height,
|
||||
color: Colors.white.withValues(alpha: opacity),
|
||||
letterSpacing: -0.2,
|
||||
);
|
||||
}
|
||||
|
||||
class _DemoBadge extends StatelessWidget {
|
||||
const _DemoBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF59E0B), borderRadius: BorderRadius.circular(999)),
|
||||
child: const Text('DEMO', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF1F2937), letterSpacing: 1)),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ shared pieces
|
||||
|
||||
/// Scrollable area whose child fills at least the screen height (so Spacers work and nothing overflows).
|
||||
class _FillScroll extends StatelessWidget {
|
||||
final Widget child;
|
||||
const _FillScroll({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
CustomScrollView(slivers: [SliverFillRemaining(hasScrollBody: false, child: child)]);
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
final String text;
|
||||
const _Title(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
return Text(text,
|
||||
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w600, color: p.title, letterSpacing: -0.3));
|
||||
}
|
||||
}
|
||||
|
||||
/// Sun / moon button that switches the sign-in screens between light and dark.
|
||||
class _ThemeToggle extends StatelessWidget {
|
||||
final bool onDark;
|
||||
const _ThemeToggle({this.onDark = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
final color = onDark ? Colors.white : p.muted;
|
||||
return IconButton(
|
||||
tooltip: p.dark ? 'Light theme' : 'Dark theme',
|
||||
onPressed: () => AuthThemeMode.set(p.dark ? ThemeMode.light : ThemeMode.dark),
|
||||
icon: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
transitionBuilder: (child, anim) => RotationTransition(turns: anim, child: FadeTransition(opacity: anim, child: child)),
|
||||
child: Icon(p.dark ? Icons.light_mode_outlined : Icons.dark_mode_outlined,
|
||||
key: ValueKey(p.dark), color: color, size: 22),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Footer extends StatelessWidget {
|
||||
final EdgeInsets padding;
|
||||
const _Footer({this.padding = const EdgeInsets.fromLTRB(24, 8, 24, 16)});
|
||||
|
||||
Future<void> _open(String url) => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
final style = TextStyle(fontSize: 12, color: p.muted);
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Wrap(alignment: WrapAlignment.center, spacing: 18, children: [
|
||||
GestureDetector(onTap: () => _open('https://scalar360.com/terms'), child: Text('Terms of use', style: style)),
|
||||
GestureDetector(onTap: () => _open('https://scalar360.com/privacy'), child: Text('Privacy', style: style)),
|
||||
Text('© ${DateTime.now().year} Scalar360', style: style),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Second step: a back arrow with the identifier entered on the first step.
|
||||
class AuthIdentity extends StatelessWidget {
|
||||
final String value;
|
||||
final VoidCallback onBack;
|
||||
const AuthIdentity({super.key, required this.value, required this.onBack});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
final row = Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(Icons.arrow_back, size: 18, color: p.text),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text(value, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14.5, color: p.text))),
|
||||
]);
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
onTap: onBack,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
decoration: BoxDecoration(border: Border.all(color: p.border), borderRadius: BorderRadius.circular(999)),
|
||||
child: row,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rounded, outlined text field with a leading icon.
|
||||
class AuthField extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final IconData? icon;
|
||||
final bool secret;
|
||||
final bool autofocus;
|
||||
final TextInputType? keyboardType;
|
||||
final TextInputAction? action;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final bool error;
|
||||
|
||||
const AuthField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.hint,
|
||||
this.icon,
|
||||
this.secret = false,
|
||||
this.autofocus = false,
|
||||
this.keyboardType,
|
||||
this.action,
|
||||
this.onSubmitted,
|
||||
this.error = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AuthField> createState() => _AuthFieldState();
|
||||
}
|
||||
|
||||
class _AuthFieldState extends State<AuthField> {
|
||||
bool _hidden = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
final base = widget.error ? p.danger : p.border;
|
||||
final focus = widget.error ? p.danger : p.accent;
|
||||
InputBorder border(Color c, double w) =>
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: c, width: w));
|
||||
return TextField(
|
||||
controller: widget.controller,
|
||||
obscureText: widget.secret && _hidden,
|
||||
autofocus: widget.autofocus,
|
||||
autocorrect: false,
|
||||
enableSuggestions: !widget.secret,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.action,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
style: TextStyle(fontSize: 16, color: p.text),
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
hintStyle: TextStyle(fontSize: 16, color: p.muted),
|
||||
filled: true,
|
||||
fillColor: p.fieldFill,
|
||||
prefixIcon: widget.icon == null ? null : Icon(widget.icon, color: p.muted, size: 20),
|
||||
suffixIcon: widget.secret
|
||||
? IconButton(
|
||||
tooltip: _hidden ? 'Show' : 'Hide',
|
||||
icon: Icon(_hidden ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: p.muted, size: 20),
|
||||
onPressed: () => setState(() => _hidden = !_hidden),
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
enabledBorder: border(base, 1),
|
||||
focusedBorder: border(focus, 2),
|
||||
border: border(base, 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Red message above the field.
|
||||
class AuthError extends StatelessWidget {
|
||||
final String message;
|
||||
const AuthError(this.message, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(message, style: TextStyle(color: AuthScope.of(context).palette.danger, fontSize: 14)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Small text link.
|
||||
class AuthLink extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
const AuthLink(this.label, {super.key, this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Text(label, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: AuthScope.of(context).palette.link)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// "Keep me signed in" checkbox.
|
||||
class AuthKeepSignedIn extends StatelessWidget {
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
const AuthKeepSignedIn({super.key, required this.value, required this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
return InkWell(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(
|
||||
value: value,
|
||||
onChanged: (v) => onChanged(v ?? true),
|
||||
activeColor: p.accent,
|
||||
side: BorderSide(color: p.line, width: 1.5),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text('Keep me signed in', style: TextStyle(fontSize: 14, color: p.text)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-width button for the step, with a spinner while busy.
|
||||
class AuthButton extends StatelessWidget {
|
||||
final String label;
|
||||
final bool busy;
|
||||
final VoidCallback onPressed;
|
||||
const AuthButton({super.key, required this.label, required this.busy, required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final p = AuthScope.of(context).palette;
|
||||
return FilledButton(
|
||||
onPressed: busy ? null : onPressed,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: p.accent,
|
||||
disabledBackgroundColor: p.accent.withValues(alpha: 0.6),
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(52),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
// same family as the rest of the screen (Poppins), which a button style does not inherit by itself
|
||||
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, fontFamily: DefaultTextStyle.of(context).style.fontFamily),
|
||||
elevation: 0,
|
||||
),
|
||||
child: busy
|
||||
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2.2, color: Colors.white))
|
||||
: Text(label),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../api_service.dart';
|
||||
import '../settings.dart';
|
||||
import '../theme.dart';
|
||||
import 'auth_shell.dart';
|
||||
import 'home_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
final ApiService api;
|
||||
@@ -19,15 +18,32 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final _pin = TextEditingController();
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
/// Zoho-style: the email first, then the PIN on its own step.
|
||||
bool _askPin = false;
|
||||
bool _keep = true;
|
||||
|
||||
static final _emailPattern = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
|
||||
|
||||
void _next() {
|
||||
final email = _email.text.trim();
|
||||
if (!_emailPattern.hasMatch(email)) {
|
||||
setState(() => _error = 'Enter a valid work email address');
|
||||
return;
|
||||
}
|
||||
setState(() { _askPin = true; _error = null; });
|
||||
}
|
||||
|
||||
void _change() => setState(() { _askPin = false; _pin.clear(); _error = null; });
|
||||
|
||||
Future<void> _login() async {
|
||||
if (_email.text.trim().isEmpty || _pin.text.isEmpty) {
|
||||
setState(() => _error = 'Enter your work email and PIN');
|
||||
if (_pin.text.isEmpty) {
|
||||
setState(() => _error = 'Enter your PIN');
|
||||
return;
|
||||
}
|
||||
setState(() { _busy = true; _error = null; });
|
||||
try {
|
||||
await widget.api.login(_email.text, _pin.text);
|
||||
await widget.api.login(_email.text.trim(), _pin.text);
|
||||
await KeepSignedIn.save(_keep);
|
||||
if (!mounted) return;
|
||||
Navigator.pushReplacement(context,
|
||||
MaterialPageRoute(builder: (_) => HomeScreen(api: widget.api, settings: widget.settings)));
|
||||
@@ -38,82 +54,73 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _forgotEmail() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Use the work email HR added you with. Not sure which one? Ask your HR or manager.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
|
||||
void _forgotPin() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Your PIN is set by HR. Ask your HR or manager to reset it.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
if (_askPin) {
|
||||
return AuthShell(
|
||||
appName: 'Scalar Presence',
|
||||
product: 'Presence',
|
||||
tagline: 'Check in, apply for leave and see your payslips.',
|
||||
title: 'Enter PIN',
|
||||
children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(color: AppColors.emerald, borderRadius: BorderRadius.circular(12)),
|
||||
child: const Icon(Icons.badge_outlined, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('Scalar360', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.navy)),
|
||||
]),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined, color: AppColors.muted),
|
||||
onPressed: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (_) => SettingsScreen(settings: widget.settings))),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 28),
|
||||
const Text('Employee sign in',
|
||||
style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: AppColors.navy)),
|
||||
const SizedBox(height: 4),
|
||||
const Text('Use your work email and the PIN from HR', style: TextStyle(color: AppColors.muted)),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: _email,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(labelText: 'Work email', prefixIcon: Icon(Icons.mail_outline)),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
controller: _pin,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
onSubmitted: (_) => _login(),
|
||||
decoration: const InputDecoration(labelText: 'PIN', prefixIcon: Icon(Icons.pin_outlined)),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: AppColors.danger.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12)),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(_error!, style: const TextStyle(color: AppColors.danger))),
|
||||
]),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 22),
|
||||
FilledButton(
|
||||
onPressed: _busy ? null : _login,
|
||||
child: _busy
|
||||
? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Sign in'),
|
||||
),
|
||||
AuthIdentity(value: _email.text.trim(), onBack: _busy ? () {} : _change),
|
||||
const SizedBox(height: 16),
|
||||
Text('Server: ${widget.settings.baseUrl}',
|
||||
textAlign: TextAlign.center, style: const TextStyle(color: AppColors.muted, fontSize: 12)),
|
||||
if (_error != null) AuthError(_error!),
|
||||
AuthField(
|
||||
key: const ValueKey('pin'),
|
||||
controller: _pin,
|
||||
hint: 'PIN',
|
||||
icon: Icons.lock_outline,
|
||||
secret: true,
|
||||
autofocus: true,
|
||||
error: _error != null,
|
||||
keyboardType: TextInputType.number,
|
||||
action: TextInputAction.done,
|
||||
onSubmitted: (_) => _login(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AuthLink('Forgot PIN?', onPressed: _forgotPin),
|
||||
AuthKeepSignedIn(value: _keep, onChanged: (v) => setState(() => _keep = v)),
|
||||
const SizedBox(height: 20),
|
||||
AuthButton(label: 'Sign in', busy: _busy, onPressed: _login),
|
||||
],
|
||||
);
|
||||
}
|
||||
return AuthShell(
|
||||
appName: 'Scalar Presence',
|
||||
product: 'Presence',
|
||||
tagline: 'Check in, apply for leave and see your payslips.',
|
||||
children: [
|
||||
if (_error != null) AuthError(_error!),
|
||||
AuthField(
|
||||
key: const ValueKey('email'),
|
||||
controller: _email,
|
||||
hint: 'Work email',
|
||||
icon: Icons.mail_outline,
|
||||
error: _error != null,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
action: TextInputAction.next,
|
||||
onSubmitted: (_) => _next(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AuthLink('Forgot email?', onPressed: _forgotEmail),
|
||||
const SizedBox(height: 28),
|
||||
AuthButton(label: 'Next', busy: false, onPressed: _next),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,6 @@ class SettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
late final TextEditingController _url = TextEditingController(text: widget.settings.baseUrl);
|
||||
|
||||
void _save() {
|
||||
widget.settings.baseUrl = _url.text.trim();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Saved')));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name = widget.settings.name;
|
||||
@@ -36,22 +28,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: Text(widget.settings.isManager ? 'Manager' : 'Employee'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('Server URL', style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.navy)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _url,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(prefixIcon: Icon(Icons.link), hintText: Settings.defaultBaseUrl),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text('The Scalar360 API base URL. Default is api.scalar360.in.',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.muted)),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(onPressed: _save, child: const Text('Save')),
|
||||
if (widget.onLogout != null) ...[
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 20),
|
||||
OutlinedButton.icon(
|
||||
onPressed: widget.onLogout,
|
||||
icon: const Icon(Icons.logout, color: AppColors.danger),
|
||||
|
||||
@@ -2,19 +2,17 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Persisted app settings + employee session.
|
||||
class Settings {
|
||||
static const _kBaseUrl = 'base_url';
|
||||
static const _kToken = 'emp_token';
|
||||
static const _kName = 'emp_name';
|
||||
static const _kRole = 'emp_role';
|
||||
|
||||
static const defaultBaseUrl = 'https://api.scalar360.in';
|
||||
/// Scalar360 API. Fixed per build (not shown to or changed by the user); a test build points at demo with
|
||||
/// `--dart-define=API_BASE_URL=https://demo.scalar360.in`.
|
||||
static const baseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.scalar360.com');
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
Future<void> load() async => _prefs = await SharedPreferences.getInstance();
|
||||
|
||||
String get baseUrl => (_prefs.getString(_kBaseUrl) ?? defaultBaseUrl).trim();
|
||||
set baseUrl(String v) => _prefs.setString(_kBaseUrl, v.trim());
|
||||
|
||||
String? get token => _prefs.getString(_kToken);
|
||||
String? get name => _prefs.getString(_kName);
|
||||
String get role => _prefs.getString(_kRole) ?? 'EMPLOYEE';
|
||||
|
||||
@@ -38,7 +38,7 @@ ThemeData buildTheme() {
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
scaffoldBackgroundColor: AppColors.surface,
|
||||
fontFamily: 'Roboto',
|
||||
fontFamily: 'Poppins',
|
||||
);
|
||||
return base.copyWith(
|
||||
appBarTheme: const AppBarTheme(
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0d58d1fa1fddf6588251ccf2819f3fb3b6d0452c1b2813ac7413a05592bfbefe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -17,14 +41,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -37,10 +77,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||
sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+4"
|
||||
version: "0.3.5+5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -49,6 +89,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -77,18 +125,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
version: "0.9.4+1"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.5"
|
||||
version: "0.9.5+1"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,10 +149,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+5"
|
||||
version: "0.9.3+6"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -118,6 +166,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.4"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -126,6 +182,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -159,18 +223,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_apple
|
||||
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
|
||||
sha256: "3b3189beba8b59b2a0eee970b466e991e4ac1564a9034f82a825a3c45c4be1fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.14"
|
||||
version: "2.3.15"
|
||||
geolocator_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_platform_interface
|
||||
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
|
||||
sha256: b640b6b687b14c66f51fe1130b1c2e8babbd408e0158052b92006b6675ca77fc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.8"
|
||||
version: "4.4.0"
|
||||
geolocator_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -187,6 +251,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.5"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.7"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -203,22 +275,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||
sha256: e71b1bd9e40124041128d60fc3034c7b1598e1bba4e570bba0c70301c817013a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.2.4"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||
sha256: "8a7823d5d3e884f7780829d85d92110d072bdcd9ed48e2be158d2abe91a912d4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+19"
|
||||
version: "0.8.13+25"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -231,10 +311,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||
sha256: "4fb88d4350ffc208c2a09fc321db1f80282688716b8cfe10c8c80acc5ae8934a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+6"
|
||||
version: "0.8.13+10"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -255,10 +335,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||
sha256: "2dee4b4e3be429db71472c9c559c981498070a61af1ba82e0e4986fc5dadc042"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
version: "2.11.2"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -275,6 +355,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.12.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -303,10 +391,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -339,14 +427,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
version: "3.2.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -355,16 +451,24 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
version: "2.5.6"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
|
||||
@@ -375,10 +479,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
version: "2.5.7"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -448,6 +552,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.33"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.6"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -480,6 +656,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
sdks:
|
||||
dart: ">=3.12.0 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||
|
||||
@@ -10,14 +10,49 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
http: ^1.2.0
|
||||
url_launcher: ^6.3.0
|
||||
shared_preferences: ^2.2.0
|
||||
# 2.5.x crashes at start on release builds with Flutter 3.44 (NoSuchMethodError via JNI); POS uses 2.4.27 fine
|
||||
shared_preferences_android: 2.4.27
|
||||
intl: ^0.19.0
|
||||
geolocator: ^13.0.1
|
||||
image_picker: ^1.1.2
|
||||
cupertino_icons: ^1.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.14.0
|
||||
flutter_native_splash: ^2.4.0
|
||||
flutter_lints: ^4.0.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: false
|
||||
image_path: "assets/brand/scalar-mark.png"
|
||||
adaptive_icon_background: "#FFFFFF"
|
||||
adaptive_icon_foreground: "assets/brand/scalar-mark-maskable.png"
|
||||
min_sdk_android: 21
|
||||
|
||||
flutter_native_splash:
|
||||
color: "#FFFFFF"
|
||||
image: assets/brand/scalar-mark.png
|
||||
android_12:
|
||||
color: "#FFFFFF"
|
||||
image: assets/brand/scalar-mark-android12.png
|
||||
icon_background_color: "#FFFFFF"
|
||||
ios: false
|
||||
web: false
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/brand/
|
||||
fonts:
|
||||
- family: Poppins
|
||||
fonts:
|
||||
- asset: assets/fonts/poppins/Poppins-Regular.ttf
|
||||
- asset: assets/fonts/poppins/Poppins-Medium.ttf
|
||||
weight: 500
|
||||
- asset: assets/fonts/poppins/Poppins-SemiBold.ttf
|
||||
weight: 600
|
||||
- asset: assets/fonts/poppins/Poppins-Bold.ttf
|
||||
weight: 700
|
||||
|
||||
@@ -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:scalar_attendance/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);
|
||||
});
|
||||
}
|
||||