diff --git a/android/app/build.gradle b/android/app/build.gradle index e542ec3..d581526 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,7 +7,8 @@ plugins { android { namespace = "com.example.fluttervoice2text" - compileSdk = flutter.compileSdkVersion + // compileSdk = flutter.compileSdkVersion + compileSdk = 35 ndkVersion = flutter.ndkVersion compileOptions { @@ -24,7 +25,8 @@ android { applicationId = "com.example.fluttervoice2text" // 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 + // minSdk = flutter.minSdkVersion + minSdk 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..3d175e9 Binary files /dev/null and b/images/logo.png differ diff --git a/images/splashatas.png b/images/splashatas.png new file mode 100644 index 0000000..7adff61 Binary files /dev/null and b/images/splashatas.png differ diff --git a/images/splashbawah.png b/images/splashbawah.png new file mode 100644 index 0000000..33d378b Binary files /dev/null and b/images/splashbawah.png differ diff --git a/images/vektoratas.png b/images/vektoratas.png new file mode 100644 index 0000000..f426035 Binary files /dev/null and b/images/vektoratas.png differ diff --git a/lib/app/app_extension.dart b/lib/app/app_extension.dart new file mode 100644 index 0000000..5ae765a --- /dev/null +++ b/lib/app/app_extension.dart @@ -0,0 +1,5 @@ +import 'package:jiffy/jiffy.dart'; + +String formatDateJiffy(String serverDate) { + return Jiffy.parse(serverDate).format(pattern: 'dd-MM-yyyy'); +} \ No newline at end of file diff --git a/lib/app/constant.dart b/lib/app/constant.dart new file mode 100644 index 0000000..9906f44 --- /dev/null +++ b/lib/app/constant.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +class Constant { + // static double designHeight = 1024; + // static double designWidth = 1440; + + // base url + static String baseURL = "http://devone.aplikasi.web.id/"; + + static String bearerName = "scan-ktp"; + + static double designHeightPhone = 844; + static double designWidthPhone = 390; + + // color theme + static Color inputanGrey = const Color(0xff637381); + static Color bgButton = const Color(0xFF0098DA); + static Color bgRed = const Color(0xffFF4842).withOpacity(0.08); + static Color bgGrey = const Color(0xffF9F9F9); + static Color bgBlue = Colors.blue; + static Color bgIcon = const Color(0xffA8CF45); + static Color textCardGrey = const Color.fromRGBO(0, 0, 0, 0.60); + + static Color textBlack = const Color(0xff212B36); + static Color textWhite = Color(0xffFDFDFD); + static Color textRed = Color(0xffFF4842); + // background upload file + static Color bgUploadFile = Color.fromRGBO(207, 207, 207, 0.20); + + // size convertion + static double getActualXPhone({ + required BuildContext context, + required double x, + }) { + return x / designWidthPhone * MediaQuery.of(context).size.width; + } + + static double getActualYPhone({ + required BuildContext context, + required double y, + }) { + return y / designHeightPhone * MediaQuery.of(context).size.height; + } + + // typography + static TextStyle titleRiwayat({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 20), + fontWeight: FontWeight.w500, + ); + } + + static TextStyle cardText({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 14), + fontWeight: FontWeight.w400, + ); + } + + static TextStyle title_700({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 36), + fontWeight: FontWeight.w700, + ); + } + + static TextStyle title_400({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 16), + fontWeight: FontWeight.w400, + ); + } + + static TextStyle titleInputan500({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 16), + fontWeight: FontWeight.w500, + ); + } + + static TextStyle titlePosisiHP({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 18), + fontWeight: FontWeight.normal, + ); + } + + static TextStyle titleInputan600({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 16), + fontWeight: FontWeight.w600, + ); + } + + static TextStyle titleButton500({required BuildContext context}) { + return TextStyle( + fontSize: Constant.getActualYPhone(context: context, y: 15), + fontWeight: FontWeight.w500, + ); + } +} diff --git a/lib/app/route.dart b/lib/app/route.dart new file mode 100644 index 0000000..f97d12c --- /dev/null +++ b/lib/app/route.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import '../screen/home/home_screen.dart'; +import '../screen/login/login_screen.dart'; +import '../screen/splash/splash_screen.dart'; + +const splashRoute = "/splashRoute"; +const loginRoute = "/loginRoute"; +const homeRoute = "/homeRoute"; +const scanRoute = "/scanRoute"; +const editScanRoute = "/editScanRoute"; + +class AppRoute { + static Route generateRoute(RouteSettings settings) { + // splash screen + if (settings.name == splashRoute) { + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)), + child: SplashScreen(), + ); + }); + } + + // login screen + if (settings.name == loginRoute) { + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)), + child: LoginScreen(), + ); + }); + } + + // home screen + if (settings.name == homeRoute) { + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(1.0), padding: EdgeInsets.all(0)), + child: HomeScreen(), + ); + }); + } + + return MaterialPageRoute(builder: (context) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + padding: const EdgeInsets.all(0), + textScaler: TextScaler.linear(1.0)), + child: SplashScreen(), + ); + }); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8e94089..a4e15a3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,125 +1,45 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'app/route.dart'; void main() { - runApp(const MyApp()); + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitUp, + ]).then( + (value) => runApp( + const ProviderScope( + child: 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', + title: 'Voice To Text', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, + 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( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // 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. - // - // 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). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + scrollBehavior: const MaterialScrollBehavior().copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.unknown, + }, + ), + debugShowCheckedModeBanner: false, + initialRoute: splashRoute, + onGenerateRoute: AppRoute.generateRoute, ); } } diff --git a/lib/model/auth_model.dart b/lib/model/auth_model.dart new file mode 100644 index 0000000..d935922 --- /dev/null +++ b/lib/model/auth_model.dart @@ -0,0 +1,83 @@ +class AuthModel { + final String token; + final String host; + final UserModel model; + + AuthModel({ + required this.host, + required this.token, + required this.model, + }); + + Map toJson() { + return { + 'host':host, + 'token': token, + 'model': model.toJson(), + }; + } +} + +class UserModel { + final String userId; + final String username; + final String groupDashboard; + final String defaultSampleStationId; + final String staffName; + final String isCourier; + final String timeAutoLogout; + final String ip; + final String agent; + final String version; + final String lastLogin; + final int satelliteId; + + UserModel({ + required this.userId, + required this.username, + required this.groupDashboard, + required this.defaultSampleStationId, + required this.staffName, + required this.isCourier, + required this.timeAutoLogout, + required this.ip, + required this.agent, + required this.version, + required this.lastLogin, + required this.satelliteId, + }); + + factory UserModel.fromJson(Map json) { + return UserModel( + userId: json['M_UserID'] ?? '', + username: json['M_UserUsername'] ?? '', + groupDashboard: json['M_UserGroupDashboard'] ?? '', + defaultSampleStationId: json['M_UserDefaultT_SampleStationID'] ?? '', + staffName: json['M_StaffName'] ?? '', + isCourier: json['is_courier'] ?? '', + timeAutoLogout: json['time_autologout'] ?? '', + ip: json['ip'] ?? '', + agent: json['agent'] ?? '', + version: json['version'] ?? '', + lastLogin: json['last-login'] ?? '', + satelliteId: json['M_SatelliteID'] ?? 0, + ); + } + + Map toJson() { + return { + 'M_UserID': userId, + 'M_UserUsername': username, + 'M_UserGroupDashboard': groupDashboard, + 'M_UserDefaultT_SampleStationID': defaultSampleStationId, + 'M_StaffName': staffName, + 'is_courier': isCourier, + 'time_autologout': timeAutoLogout, + 'ip': ip, + 'agent': agent, + 'version': version, + 'last-login': lastLogin, + 'M_SatelliteID': satelliteId, + }; + } +} diff --git a/lib/provider/auth_provider.dart b/lib/provider/auth_provider.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/provider/current_user_provider.dart b/lib/provider/current_user_provider.dart new file mode 100644 index 0000000..e358c65 --- /dev/null +++ b/lib/provider/current_user_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../model/auth_model.dart'; + +final currentUserProvider = StateProvider((ref) => null); \ No newline at end of file diff --git a/lib/provider/dio_provider.dart b/lib/provider/dio_provider.dart new file mode 100644 index 0000000..7596edf --- /dev/null +++ b/lib/provider/dio_provider.dart @@ -0,0 +1,4 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final dioProvider = Provider((ref) => Dio()); \ No newline at end of file diff --git a/lib/repository/auth_repository.dart b/lib/repository/auth_repository.dart new file mode 100644 index 0000000..935d84c --- /dev/null +++ b/lib/repository/auth_repository.dart @@ -0,0 +1,24 @@ +import '../model/auth_model.dart'; +import 'base_repository.dart'; + +class AuthRepository extends BaseRepository { + AuthRepository({required super.dio}); + + Future login({ + required String username, + required String host, + required String password, + }) async { + final param = {"username": username, "password": password}; + // final service = "${Constant.baseUrl}xauth/login"; + final service = "http://$host/one-api/v1/system/auth/login"; + final resp = await post(param: param, service: service); + + final result = AuthModel( + host: host, + token: resp["data"]["token"], + model: UserModel.fromJson(resp["data"]["user"]), + ); + return result; + } +} diff --git a/lib/repository/base_repository.dart b/lib/repository/base_repository.dart new file mode 100644 index 0000000..a3496a2 --- /dev/null +++ b/lib/repository/base_repository.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; + +abstract class BaseRepository { + final Dio dio; + BaseRepository({required this.dio}); + + // POST PAKE ContentType JSON + Future> post({ + required Map param, + required String service, + String? token, + }) async { + try { + final response = await dio.post( + // Constant.baseUrl + service, + service, + data: jsonEncode(param), + options: Options( + headers: token != null + ? { + HttpHeaders.contentTypeHeader: "application/json", + HttpHeaders.authorizationHeader: "Bearer $token", + } + : { + HttpHeaders.contentTypeHeader: "application/json", + }, + contentType: "application/json", + ), + ); + if (response.statusCode != 200) { + throw BaseRepositoryException( + message: "Invalid Http Response ${response.statusCode}", + ); + } + Map jsonData = jsonDecode(response.data); + if (jsonData["status"] != "OK") { + throw BaseRepositoryException( + message: jsonData["message"], + ); + } else { + return jsonData; + } + } on DioException catch (e) { + throw BaseRepositoryException(message: e.message ?? ""); + } on SocketException catch (e) { + throw BaseRepositoryException(message: e.message); + } on BaseRepositoryException catch (e) { + throw BaseRepositoryException(message: e.message); + } + } + + // GET Pake Content Type JSON + Future> get({ + // required Map param, + required String service, + String? token, + }) async { + try { + final response = await dio.get( + // Constant.baseUrl + service, + service, + // data: jsonEncode(param), + options: Options( + headers: token != null + ? { + HttpHeaders.contentTypeHeader: "application/json", + HttpHeaders.authorizationHeader: "Bearer $token", + } + : { + HttpHeaders.contentTypeHeader: "application/json", + }, + contentType: "application/json", + ), + ); + if (response.statusCode != 200) { + throw BaseRepositoryException( + message: "Invalid Http Response ${response.statusCode}", + ); + } + Map jsonData = jsonDecode(response.data); + if (jsonData["status"] != "OK") { + throw BaseRepositoryException( + message: jsonData["message"], + ); + } else { + return jsonData; + } + } on DioException catch (e) { + throw BaseRepositoryException(message: e.message ?? ""); + } on SocketException catch (e) { + throw BaseRepositoryException(message: e.message); + } on BaseRepositoryException catch (e) { + throw BaseRepositoryException(message: e.message); + } + } +} + +class BaseRepositoryException implements Exception { + final String message; + BaseRepositoryException({ + required this.message, + }); +} \ No newline at end of file diff --git a/lib/screen/home/home_screen.dart b/lib/screen/home/home_screen.dart new file mode 100644 index 0000000..ab34012 --- /dev/null +++ b/lib/screen/home/home_screen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../app/constant.dart'; +import '../../app/route.dart'; +import '../../provider/current_user_provider.dart'; + +class HomeScreen extends HookConsumerWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ + SystemUiOverlay.bottom, + ]); + + final currentUser = ref.watch(currentUserProvider); + // final username = currentUser?.model.username ?? "-"; + final host = currentUser?.host ?? ""; + final isLoading = useState(false); + final userId = currentUser?.model.userId ?? ""; + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final userID = currentUser?.model.userId ?? "0"; + if (userID == "0") { + // not logged in + Navigator.of(context) + .pushNamedAndRemoveUntil(loginRoute, (route) => false); + return; + } + }); + return () {}; + }, [currentUser]); + + // listRiwayProvider + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus!.unfocus(); + }, + child: Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Constant.bgGrey, + body: Column( + children: [ + // atas + Image.asset( + 'images/vektoratas.png', + width: double.infinity, + fit: BoxFit.cover, + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 34, + ), + ), + Text(currentUser?.model.username ?? ""), + ], + ), + ), + ); + } +} diff --git a/lib/screen/login/login_provider.dart b/lib/screen/login/login_provider.dart new file mode 100644 index 0000000..9aa8638 --- /dev/null +++ b/lib/screen/login/login_provider.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../app/constant.dart'; +import '../../model/auth_model.dart'; +import '../../provider/current_user_provider.dart'; +import '../../provider/dio_provider.dart'; +import '../../repository/auth_repository.dart'; +import '../../repository/base_repository.dart'; + +// 3. state provider +final loginProvider = StateNotifierProvider( + (ref) => LoginNotifier(ref: ref)); + +// 2. notifier +class LoginNotifier extends StateNotifier { + final Ref ref; + LoginNotifier({required this.ref}) : super(LoginStateInit()); + void login({ + required String username, + required String host, + required String password, + }) async { + try { + state = LoginStateLoading(); + final resp = await AuthRepository( + dio: ref.read(dioProvider), + ).login( + username: username, + host: host, + password: password, + ); + + // print(resp); + state = LoginStateDone(model: resp); + //Simpan ke token + final shared = await SharedPreferences.getInstance(); + final token = jsonEncode({ + "host":host, + "date": DateTime.now().toString(), + "model": resp.model, + "token": resp.token + }); + await shared.setString(Constant.bearerName, token); + ref.read(currentUserProvider.notifier).state = resp; + + // print(shared.getString(Constant.bearerName)); + } catch (e) { + if (e is BaseRepositoryException) { + state = LoginStateError(message: e.message); + } else { + state = LoginStateError(message: e.toString()); + } + } + } +} + +// 1. state +abstract class LoginState extends Equatable { + final DateTime date; + const LoginState(this.date); + @override + List get props => [date]; +} + +class LoginStateInit extends LoginState { + LoginStateInit() : super(DateTime.now()); +} + +class LoginStateLoading extends LoginState { + LoginStateLoading() : super(DateTime.now()); +} + +class LoginStateError extends LoginState { + final String message; + LoginStateError({ + required this.message, + }) : super(DateTime.now()); +} + +class LoginStateDone extends LoginState { + final AuthModel model; + LoginStateDone({ + required this.model, + }) : super(DateTime.now()); +} diff --git a/lib/screen/login/login_screen.dart b/lib/screen/login/login_screen.dart new file mode 100644 index 0000000..e22a1d5 --- /dev/null +++ b/lib/screen/login/login_screen.dart @@ -0,0 +1,421 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:toastification/toastification.dart'; +import '../../app/route.dart'; + +import '../../app/constant.dart'; +import '../../provider/current_user_provider.dart'; +import 'login_provider.dart'; + +class LoginScreen extends HookConsumerWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ + SystemUiOverlay.bottom, + ]); + + final usernameCtr = useTextEditingController( + text: "", + ); + + final passwordCtr = useTextEditingController( + text: "", + ); + + final hostCtr = useTextEditingController( + text: "", + ); + + final isLoading = useState(false); + final isSuccess = useState(false); + + ToastificationItem? toastItem; + + void showLongToast( + String title, + String message, + String typeToast, + Duration waktu, + ) { + if (typeToast == "success") { + toastItem = toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.success, + ); + } else if (typeToast == "error") { + toastItem = toastification.show( + context: context, + title: Text(title), + description: Text(message), + autoCloseDuration: waktu, + type: ToastificationType.error, + style: ToastificationStyle.fillColored, + ); + } + } + + void hideToast() { + if (toastItem != null) { + toastification.dismissAll(); + } + } + + // proses login + ref.listen(loginProvider, (prev, next) { + if (next is LoginStateLoading) { + isLoading.value = true; + } else if (next is LoginStateError) { + isLoading.value = false; + // errorMessage.value = next.message; + showLongToast( + 'Error', + next.message, + 'error', + Duration(seconds: 3), + ); + } else if (next is LoginStateDone) { + hideToast(); + isLoading.value = false; + isSuccess.value = true; + ref.read(currentUserProvider.notifier).state = next.model; + Navigator.of(context) + .pushNamedAndRemoveUntil(homeRoute, (route) => false); + } + }); + + void login() { + if (usernameCtr.text.isEmpty || + passwordCtr.text.isEmpty || + hostCtr.text.isEmpty) { + showLongToast( + 'Error', + 'Inputan wajib diisi', + 'error', + Duration(seconds: 3), + ); + } else { + // print('proses login'); + ref.read(loginProvider.notifier).login( + username: usernameCtr.text, + host: hostCtr.text, + password: passwordCtr.text, + ); + } + } + + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus!.unfocus(); + }, + child: Scaffold( + resizeToAvoidBottomInset: true, + backgroundColor: Constant.textWhite, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // atas + Image.asset( + 'images/vektoratas.png', + width: double.infinity, + fit: BoxFit.cover, + ), + + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 40, + ), + ), + // konten didalamnya + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Text( + 'Selamat Datang', + style: Constant.title_700(context: context).copyWith( + color: Constant.textBlack, + ), + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Text( + 'Silahkan masuk untuk mengakses akun Anda', + style: Constant.title_400(context: context).copyWith( + color: Constant.textBlack, + ), + ), + ), + + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 64, + ), + ), + + // inputan + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Text( + 'Username', + style: Constant.title_400(context: context).copyWith( + color: Constant.inputanGrey, + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 16, + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: TextField( + controller: usernameCtr, + decoration: InputDecoration( + hintText: "Masukkan Username", + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey, + width: 1, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.blue, + width: 2, + ), + ), + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 20, + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Text( + 'Password', + style: Constant.title_400(context: context).copyWith( + color: Constant.inputanGrey, + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 16, + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: TextField( + controller: passwordCtr, + obscureText: true, + decoration: InputDecoration( + hintText: "Masukkan Password", + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey, + width: 1, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.blue, + width: 2, + ), + ), + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 20, + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: Text( + 'Host', + style: Constant.title_400(context: context).copyWith( + color: Constant.inputanGrey, + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 16, + ), + ), + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: TextField( + controller: hostCtr, + decoration: InputDecoration( + hintText: "Masukkan Host", + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey, + width: 1, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.blue, + width: 2, + ), + ), + ), + ), + ), + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 64, + ), + ), + // inputan + + // button login + Padding( + padding: EdgeInsets.only( + left: Constant.getActualXPhone( + context: context, + x: 20, + ), + right: Constant.getActualXPhone( + context: context, + x: 20, + ), + ), + child: SizedBox( + width: double.infinity, + height: Constant.getActualYPhone( + context: context, + y: 48, + ), + child: ElevatedButton( + onPressed: () async { + (isLoading.value || (isSuccess.value == true)) + ? null + : login(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Constant.bgButton, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 8, + shadowColor: Constant.bgButton.withOpacity(0.24), + ), + child: Text( + (isLoading.value) ? 'Loading...' : 'LOGIN', + style: Constant.titleButton500(context: context).copyWith( + color: Constant.textWhite, + ), + ), + ), + ), + ), + // konten didalamnya + + SizedBox( + height: Constant.getActualYPhone( + context: context, + y: 60, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screen/splash/splash_screen.dart b/lib/screen/splash/splash_screen.dart new file mode 100644 index 0000000..64bfa2b --- /dev/null +++ b/lib/screen/splash/splash_screen.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../app/constant.dart'; +import '../../app/route.dart'; +import '../../model/auth_model.dart'; +import '../../provider/current_user_provider.dart'; + +class SplashScreen extends HookConsumerWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ + SystemUiOverlay.bottom, + ]); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final shared = await SharedPreferences.getInstance(); + final bearerString = shared.getString(Constant.bearerName); + + if (bearerString == null || bearerString == "null") { + Timer(const Duration(seconds: 3), () { + Navigator.of(context).pushNamedAndRemoveUntil( + loginRoute, + (route) => false, + ); + }); + return; + } + + final xmodel = jsonDecode(bearerString); + if (xmodel == null) return; + + final authModel = AuthModel( + host: xmodel["host"], + token: xmodel["token"], + model: UserModel( + userId: xmodel["model"]['M_UserID'], + username: xmodel["model"]['M_UserUsername'], + groupDashboard: xmodel["model"]['M_UserGroupDashboard'], + defaultSampleStationId: xmodel["model"] + ['M_UserDefaultT_SampleStationID'], + staffName: xmodel["model"]['M_StaffName'], + isCourier: xmodel["model"]['is_courier'], + timeAutoLogout: xmodel["model"]['time_autologout'], + ip: xmodel["model"]['ip'], + agent: xmodel["model"]['agent'], + version: xmodel["model"]['version'], + lastLogin: xmodel["model"]['last-login'], + satelliteId: xmodel["model"]['M_SatelliteID'], + ), + ); + + ref.read(currentUserProvider.notifier).state = authModel; + + Timer(const Duration(seconds: 3), () { + Navigator.of(context).pushNamedAndRemoveUntil( + homeRoute, + (route) => false, + ); + }); + }); + return () {}; + }, []); + + return Scaffold( + backgroundColor: Constant.textWhite, + body: Column( + children: [ + // Bagian atas + Align( + alignment: Alignment.topRight, + child: SizedBox( + width: Constant.getActualXPhone(context: context, x: 246), + child: Image.asset( + 'images/splashatas.png', + fit: BoxFit.fitWidth, + ), + ), + ), + Spacer(), + // Logo di tengah + Center( + child: Image.asset( + 'images/logo.png', + width: Constant.getActualXPhone(context: context, x: 164), + ), + ), + Spacer(), + // Bagian bawah + Align( + alignment: Alignment.bottomLeft, + child: SizedBox( + width: Constant.getActualXPhone(context: context, x: 246), + child: Image.asset( + 'images/splashbawah.png', + fit: BoxFit.fitWidth, + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 5dcd897..3d18441 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,9 +73,11 @@ flutter: 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 + assets: + - images/logo.png + - images/splashatas.png + - images/splashbawah.png + - images/vektoratas.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images