Compare commits

..
15 Commits
Author SHA1 Message Date
efrilm 6892895021 sync bloc 2025-10-24 22:25:01 +07:00
efrilm 4fdd1e44f8 product repo 2025-10-24 22:03:35 +07:00
efrilm dea5de8828 sync page 2025-10-24 20:25:30 +07:00
efrilm 71fa4823fc category repo 2025-10-24 20:06:42 +07:00
efrilm 683fff6eeb outlet dialog 2025-10-24 14:28:04 +07:00
efrilm 13b1b6e6b8 search text field 2025-10-24 14:00:53 +07:00
efrilm 4dbdcadeaf current outlet 2025-10-24 13:55:00 +07:00
efrilm 97c03bcf96 change name login form 2025-10-24 02:04:28 +07:00
efrilm 05609d592d main page 2025-10-24 02:02:21 +07:00
efrilm 59062f9af1 auth state 2025-10-24 02:02:00 +07:00
efrilm ca5d2a58e7 login 2025-10-24 01:16:50 +07:00
efrilm 5d9197c986 color and theme 2025-10-23 23:20:44 +07:00
efrilm 9a61cbecc4 crashlytic interceptro 2025-10-23 23:13:39 +07:00
efrilm cf8f0b373d logo, fonts, firebase analytic 2025-10-23 23:06:02 +07:00
efrilm 4fee7c4bb7 base project 2025-10-23 22:16:53 +07:00
208 changed files with 24471 additions and 359 deletions
+15 -23
View File
@@ -1,28 +1,20 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
sort_pub_dependencies: false
prefer_relative_imports: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
errors:
missing_required_param: error
missing_return: error
must_be_immutable: error
sort_unnamed_constructors_first: ignore
invalid_annotation_target: ignore
use_build_context_synchronously: ignore
deprecated_member_use: ignore
exclude:
- test/generated/**
- "**/**.g.dart"
- "**/**.freezed.dart"
+10 -2
View File
@@ -3,10 +3,11 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.apskel_pos_flutter_v2"
namespace = "com.apskel.pos"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +22,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.apskel_pos_flutter_v2"
applicationId = "com.apskel.pos"
// 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
@@ -42,3 +43,10 @@ android {
flutter {
source = "../.."
}
dependencies {
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.4.0"))
implementation("com.google.firebase:firebase-analytics")
}
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "765730035527",
"project_id": "apskel-pos-v2",
"storage_bucket": "apskel-pos-v2.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:765730035527:android:498defd7071336dd241e62",
"android_client_info": {
"package_name": "com.apskel.pos"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAOZwVSQwUeeM9BjcyTOK9GUh8AmTWucuc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
+13 -2
View File
@@ -1,8 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- Izin khusus untuk akses foto (media images) di Android 33 ke atas -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:label="apskel_pos_flutter_v2"
android:label="Apskel POS"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -1,4 +1,4 @@
package com.example.apskel_pos_flutter_v2
package com.apskel.pos
import io.flutter.embedding.android.FlutterActivity
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -20,6 +20,7 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.gms.google-services") version "4.4.4" apply false
}
include(":app")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+2 -2
View File
@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -1,122 +1 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+15
View File
@@ -0,0 +1,15 @@
# Generate: dart run flutter_launcher_icons -f launcher_icon.yaml
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/images/logo.png"
remove_alpha_ios: true
min_sdk_android: 21 # android min sdk min:16, default 21
web:
generate: true
image_path: "assets/images/logo.png"
windows:
generate: true
image_path: "assets/images/logo.png"
icon_size: 48
+66
View File
@@ -0,0 +1,66 @@
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../domain/auth/auth.dart';
import '../../domain/outlet/outlet.dart';
part 'auth_event.dart';
part 'auth_state.dart';
part 'auth_bloc.freezed.dart';
@injectable
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final IAuthRepository _repository;
final IOutletRepository _outletRepository;
AuthBloc(this._repository, this._outletRepository)
: super(AuthState.initial()) {
on<AuthEvent>(_onAuthEvent);
}
Future<void> _onAuthEvent(AuthEvent event, Emitter<AuthState> emit) {
return event.map(
fetchCurrentUser: (e) async {
emit(state.copyWith(failureOption: none()));
final token = await _repository.hasToken();
final failureOrAuth = await _repository.currentUser();
failureOrAuth.fold(
(f) => emit(
state.copyWith(
failureOption: optionOf(f),
status: token
? AuthStatus.authenticated()
: AuthStatus.unauthenticated(),
),
),
(user) => emit(
state.copyWith(
user: user,
status: token
? AuthStatus.authenticated()
: AuthStatus.unauthenticated(),
),
),
);
add(AuthEvent.fetchCurrentOutlet(state.user.outletId));
},
fetchCurrentOutlet: (e) async {
emit(state.copyWith(failureOutletOption: none()));
final failureOrOutlet = await _outletRepository.getOutletById(
e.outletId,
);
failureOrOutlet.fold(
(f) => emit(state.copyWith(failureOutletOption: optionOf(f))),
(outlet) => emit(state.copyWith(outlet: outlet)),
);
},
);
}
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
part of 'auth_bloc.dart';
@freezed
class AuthEvent with _$AuthEvent {
const factory AuthEvent.fetchCurrentUser() = _FetchCurrentUser;
const factory AuthEvent.fetchCurrentOutlet(String outletId) =
_FetchCurrentOutlet;
}
+33
View File
@@ -0,0 +1,33 @@
part of 'auth_bloc.dart';
@freezed
class AuthState with _$AuthState {
const AuthState._();
const factory AuthState({
required User user,
required Outlet outlet,
@Default(AuthStatus.initial()) AuthStatus status,
required Option<AuthFailure> failureOption,
required Option<OutletFailure> failureOutletOption,
@Default(false) bool isFetching,
@Default(false) bool isFetchingOutlet,
}) = _AuthState;
factory AuthState.initial() => AuthState(
user: User.empty(),
failureOption: none(),
outlet: Outlet.empty(),
failureOutletOption: none(),
);
bool get isAuthenticated => status == const AuthStatus.authenticated();
bool get isInitial => status == const AuthStatus.initial();
}
@freezed
sealed class AuthStatus with _$AuthStatus {
const factory AuthStatus.authenticated() = _Authenticated;
const factory AuthStatus.unauthenticated() = _Unauthenticated;
const factory AuthStatus.initial() = _Initial;
}
@@ -0,0 +1,62 @@
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/auth/auth.dart';
part 'login_form_event.dart';
part 'login_form_state.dart';
part 'login_form_bloc.freezed.dart';
@injectable
class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
final IAuthRepository _authRepository;
LoginFormBloc(this._authRepository) : super(LoginFormState.initial()) {
on<LoginFormEvent>(_onLoginFormEvent);
}
Future<void> _onLoginFormEvent(
LoginFormEvent event,
Emitter<LoginFormState> emit,
) {
return event.map(
emailChanged: (e) async {
emit(state.copyWith(email: e.email, failureOrLoginOption: none()));
},
passwordChanged: (e) async {
emit(
state.copyWith(password: e.password, failureOrLoginOption: none()),
);
},
submitted: (e) async {
Either<AuthFailure, Login>? failureOrLogin;
emit(state.copyWith(isSubmitting: true, failureOrLoginOption: none()));
final emailValid = state.email.isNotEmpty;
final passwordValid = state.password.isNotEmpty;
log(
'emailValid: $emailValid, passwordValid: $passwordValid, email: ${state.email}, password: ${state.password}',
);
if (emailValid && passwordValid) {
failureOrLogin = await _authRepository.login(
email: state.email,
password: state.password,
);
emit(
state.copyWith(
isSubmitting: false,
failureOrLoginOption: optionOf(failureOrLogin),
),
);
}
emit(state.copyWith(showErrorMessages: true, isSubmitting: false));
},
);
}
}
@@ -0,0 +1,734 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'login_form_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$LoginFormEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String email) emailChanged,
required TResult Function(String password) passwordChanged,
required TResult Function() submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String email)? emailChanged,
TResult? Function(String password)? passwordChanged,
TResult? Function()? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String email)? emailChanged,
TResult Function(String password)? passwordChanged,
TResult Function()? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_EmailChanged value) emailChanged,
required TResult Function(_PasswordChanged value) passwordChanged,
required TResult Function(_Submitted value) submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_EmailChanged value)? emailChanged,
TResult? Function(_PasswordChanged value)? passwordChanged,
TResult? Function(_Submitted value)? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_EmailChanged value)? emailChanged,
TResult Function(_PasswordChanged value)? passwordChanged,
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoginFormEventCopyWith<$Res> {
factory $LoginFormEventCopyWith(
LoginFormEvent value,
$Res Function(LoginFormEvent) then,
) = _$LoginFormEventCopyWithImpl<$Res, LoginFormEvent>;
}
/// @nodoc
class _$LoginFormEventCopyWithImpl<$Res, $Val extends LoginFormEvent>
implements $LoginFormEventCopyWith<$Res> {
_$LoginFormEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$EmailChangedImplCopyWith<$Res> {
factory _$$EmailChangedImplCopyWith(
_$EmailChangedImpl value,
$Res Function(_$EmailChangedImpl) then,
) = __$$EmailChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({String email});
}
/// @nodoc
class __$$EmailChangedImplCopyWithImpl<$Res>
extends _$LoginFormEventCopyWithImpl<$Res, _$EmailChangedImpl>
implements _$$EmailChangedImplCopyWith<$Res> {
__$$EmailChangedImplCopyWithImpl(
_$EmailChangedImpl _value,
$Res Function(_$EmailChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? email = null}) {
return _then(
_$EmailChangedImpl(
null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$EmailChangedImpl implements _EmailChanged {
const _$EmailChangedImpl(this.email);
@override
final String email;
@override
String toString() {
return 'LoginFormEvent.emailChanged(email: $email)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$EmailChangedImpl &&
(identical(other.email, email) || other.email == email));
}
@override
int get hashCode => Object.hash(runtimeType, email);
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$EmailChangedImplCopyWith<_$EmailChangedImpl> get copyWith =>
__$$EmailChangedImplCopyWithImpl<_$EmailChangedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String email) emailChanged,
required TResult Function(String password) passwordChanged,
required TResult Function() submitted,
}) {
return emailChanged(email);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String email)? emailChanged,
TResult? Function(String password)? passwordChanged,
TResult? Function()? submitted,
}) {
return emailChanged?.call(email);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String email)? emailChanged,
TResult Function(String password)? passwordChanged,
TResult Function()? submitted,
required TResult orElse(),
}) {
if (emailChanged != null) {
return emailChanged(email);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_EmailChanged value) emailChanged,
required TResult Function(_PasswordChanged value) passwordChanged,
required TResult Function(_Submitted value) submitted,
}) {
return emailChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_EmailChanged value)? emailChanged,
TResult? Function(_PasswordChanged value)? passwordChanged,
TResult? Function(_Submitted value)? submitted,
}) {
return emailChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_EmailChanged value)? emailChanged,
TResult Function(_PasswordChanged value)? passwordChanged,
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) {
if (emailChanged != null) {
return emailChanged(this);
}
return orElse();
}
}
abstract class _EmailChanged implements LoginFormEvent {
const factory _EmailChanged(final String email) = _$EmailChangedImpl;
String get email;
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$EmailChangedImplCopyWith<_$EmailChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$PasswordChangedImplCopyWith<$Res> {
factory _$$PasswordChangedImplCopyWith(
_$PasswordChangedImpl value,
$Res Function(_$PasswordChangedImpl) then,
) = __$$PasswordChangedImplCopyWithImpl<$Res>;
@useResult
$Res call({String password});
}
/// @nodoc
class __$$PasswordChangedImplCopyWithImpl<$Res>
extends _$LoginFormEventCopyWithImpl<$Res, _$PasswordChangedImpl>
implements _$$PasswordChangedImplCopyWith<$Res> {
__$$PasswordChangedImplCopyWithImpl(
_$PasswordChangedImpl _value,
$Res Function(_$PasswordChangedImpl) _then,
) : super(_value, _then);
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? password = null}) {
return _then(
_$PasswordChangedImpl(
null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$PasswordChangedImpl implements _PasswordChanged {
const _$PasswordChangedImpl(this.password);
@override
final String password;
@override
String toString() {
return 'LoginFormEvent.passwordChanged(password: $password)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PasswordChangedImpl &&
(identical(other.password, password) ||
other.password == password));
}
@override
int get hashCode => Object.hash(runtimeType, password);
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PasswordChangedImplCopyWith<_$PasswordChangedImpl> get copyWith =>
__$$PasswordChangedImplCopyWithImpl<_$PasswordChangedImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String email) emailChanged,
required TResult Function(String password) passwordChanged,
required TResult Function() submitted,
}) {
return passwordChanged(password);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String email)? emailChanged,
TResult? Function(String password)? passwordChanged,
TResult? Function()? submitted,
}) {
return passwordChanged?.call(password);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String email)? emailChanged,
TResult Function(String password)? passwordChanged,
TResult Function()? submitted,
required TResult orElse(),
}) {
if (passwordChanged != null) {
return passwordChanged(password);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_EmailChanged value) emailChanged,
required TResult Function(_PasswordChanged value) passwordChanged,
required TResult Function(_Submitted value) submitted,
}) {
return passwordChanged(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_EmailChanged value)? emailChanged,
TResult? Function(_PasswordChanged value)? passwordChanged,
TResult? Function(_Submitted value)? submitted,
}) {
return passwordChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_EmailChanged value)? emailChanged,
TResult Function(_PasswordChanged value)? passwordChanged,
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) {
if (passwordChanged != null) {
return passwordChanged(this);
}
return orElse();
}
}
abstract class _PasswordChanged implements LoginFormEvent {
const factory _PasswordChanged(final String password) = _$PasswordChangedImpl;
String get password;
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PasswordChangedImplCopyWith<_$PasswordChangedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$SubmittedImplCopyWith<$Res> {
factory _$$SubmittedImplCopyWith(
_$SubmittedImpl value,
$Res Function(_$SubmittedImpl) then,
) = __$$SubmittedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$SubmittedImplCopyWithImpl<$Res>
extends _$LoginFormEventCopyWithImpl<$Res, _$SubmittedImpl>
implements _$$SubmittedImplCopyWith<$Res> {
__$$SubmittedImplCopyWithImpl(
_$SubmittedImpl _value,
$Res Function(_$SubmittedImpl) _then,
) : super(_value, _then);
/// Create a copy of LoginFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$SubmittedImpl implements _Submitted {
const _$SubmittedImpl();
@override
String toString() {
return 'LoginFormEvent.submitted()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$SubmittedImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String email) emailChanged,
required TResult Function(String password) passwordChanged,
required TResult Function() submitted,
}) {
return submitted();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String email)? emailChanged,
TResult? Function(String password)? passwordChanged,
TResult? Function()? submitted,
}) {
return submitted?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String email)? emailChanged,
TResult Function(String password)? passwordChanged,
TResult Function()? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_EmailChanged value) emailChanged,
required TResult Function(_PasswordChanged value) passwordChanged,
required TResult Function(_Submitted value) submitted,
}) {
return submitted(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_EmailChanged value)? emailChanged,
TResult? Function(_PasswordChanged value)? passwordChanged,
TResult? Function(_Submitted value)? submitted,
}) {
return submitted?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_EmailChanged value)? emailChanged,
TResult Function(_PasswordChanged value)? passwordChanged,
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted(this);
}
return orElse();
}
}
abstract class _Submitted implements LoginFormEvent {
const factory _Submitted() = _$SubmittedImpl;
}
/// @nodoc
mixin _$LoginFormState {
String get email => throw _privateConstructorUsedError;
String get password => throw _privateConstructorUsedError;
Option<Either<AuthFailure, Login>> get failureOrLoginOption =>
throw _privateConstructorUsedError;
bool get isSubmitting => throw _privateConstructorUsedError;
bool get showErrorMessages => throw _privateConstructorUsedError;
/// Create a copy of LoginFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoginFormStateCopyWith<LoginFormState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoginFormStateCopyWith<$Res> {
factory $LoginFormStateCopyWith(
LoginFormState value,
$Res Function(LoginFormState) then,
) = _$LoginFormStateCopyWithImpl<$Res, LoginFormState>;
@useResult
$Res call({
String email,
String password,
Option<Either<AuthFailure, Login>> failureOrLoginOption,
bool isSubmitting,
bool showErrorMessages,
});
}
/// @nodoc
class _$LoginFormStateCopyWithImpl<$Res, $Val extends LoginFormState>
implements $LoginFormStateCopyWith<$Res> {
_$LoginFormStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoginFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? email = null,
Object? password = null,
Object? failureOrLoginOption = null,
Object? isSubmitting = null,
Object? showErrorMessages = null,
}) {
return _then(
_value.copyWith(
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
failureOrLoginOption: null == failureOrLoginOption
? _value.failureOrLoginOption
: failureOrLoginOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Login>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
showErrorMessages: null == showErrorMessages
? _value.showErrorMessages
: showErrorMessages // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$LoginFormStateImplCopyWith<$Res>
implements $LoginFormStateCopyWith<$Res> {
factory _$$LoginFormStateImplCopyWith(
_$LoginFormStateImpl value,
$Res Function(_$LoginFormStateImpl) then,
) = __$$LoginFormStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String email,
String password,
Option<Either<AuthFailure, Login>> failureOrLoginOption,
bool isSubmitting,
bool showErrorMessages,
});
}
/// @nodoc
class __$$LoginFormStateImplCopyWithImpl<$Res>
extends _$LoginFormStateCopyWithImpl<$Res, _$LoginFormStateImpl>
implements _$$LoginFormStateImplCopyWith<$Res> {
__$$LoginFormStateImplCopyWithImpl(
_$LoginFormStateImpl _value,
$Res Function(_$LoginFormStateImpl) _then,
) : super(_value, _then);
/// Create a copy of LoginFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? email = null,
Object? password = null,
Object? failureOrLoginOption = null,
Object? isSubmitting = null,
Object? showErrorMessages = null,
}) {
return _then(
_$LoginFormStateImpl(
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
failureOrLoginOption: null == failureOrLoginOption
? _value.failureOrLoginOption
: failureOrLoginOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Login>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
showErrorMessages: null == showErrorMessages
? _value.showErrorMessages
: showErrorMessages // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$LoginFormStateImpl implements _LoginFormState {
const _$LoginFormStateImpl({
required this.email,
required this.password,
required this.failureOrLoginOption,
this.isSubmitting = false,
this.showErrorMessages = false,
});
@override
final String email;
@override
final String password;
@override
final Option<Either<AuthFailure, Login>> failureOrLoginOption;
@override
@JsonKey()
final bool isSubmitting;
@override
@JsonKey()
final bool showErrorMessages;
@override
String toString() {
return 'LoginFormState(email: $email, password: $password, failureOrLoginOption: $failureOrLoginOption, isSubmitting: $isSubmitting, showErrorMessages: $showErrorMessages)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoginFormStateImpl &&
(identical(other.email, email) || other.email == email) &&
(identical(other.password, password) ||
other.password == password) &&
(identical(other.failureOrLoginOption, failureOrLoginOption) ||
other.failureOrLoginOption == failureOrLoginOption) &&
(identical(other.isSubmitting, isSubmitting) ||
other.isSubmitting == isSubmitting) &&
(identical(other.showErrorMessages, showErrorMessages) ||
other.showErrorMessages == showErrorMessages));
}
@override
int get hashCode => Object.hash(
runtimeType,
email,
password,
failureOrLoginOption,
isSubmitting,
showErrorMessages,
);
/// Create a copy of LoginFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoginFormStateImplCopyWith<_$LoginFormStateImpl> get copyWith =>
__$$LoginFormStateImplCopyWithImpl<_$LoginFormStateImpl>(
this,
_$identity,
);
}
abstract class _LoginFormState implements LoginFormState {
const factory _LoginFormState({
required final String email,
required final String password,
required final Option<Either<AuthFailure, Login>> failureOrLoginOption,
final bool isSubmitting,
final bool showErrorMessages,
}) = _$LoginFormStateImpl;
@override
String get email;
@override
String get password;
@override
Option<Either<AuthFailure, Login>> get failureOrLoginOption;
@override
bool get isSubmitting;
@override
bool get showErrorMessages;
/// Create a copy of LoginFormState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoginFormStateImplCopyWith<_$LoginFormStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,9 @@
part of 'login_form_bloc.dart';
@freezed
class LoginFormEvent with _$LoginFormEvent {
const factory LoginFormEvent.emailChanged(String email) = _EmailChanged;
const factory LoginFormEvent.passwordChanged(String password) =
_PasswordChanged;
const factory LoginFormEvent.submitted() = _Submitted;
}
@@ -0,0 +1,15 @@
part of 'login_form_bloc.dart';
@freezed
class LoginFormState with _$LoginFormState {
const factory LoginFormState({
required String email,
required String password,
required Option<Either<AuthFailure, Login>> failureOrLoginOption,
@Default(false) bool isSubmitting,
@Default(false) bool showErrorMessages,
}) = _LoginFormState;
factory LoginFormState.initial() =>
LoginFormState(email: '', password: '', failureOrLoginOption: none());
}
@@ -0,0 +1,312 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../domain/category/category.dart';
part 'category_loader_event.dart';
part 'category_loader_state.dart';
part 'category_loader_bloc.freezed.dart';
class CategoryLoaderBloc
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
final ICategoryRepository _categoryRepository;
Timer? _searchDebounce;
bool _isLoadingMore = false;
CategoryLoaderBloc(this._categoryRepository)
: super(CategoryLoaderState.initial()) {
on<CategoryLoaderEvent>(_onCategoryLoaderEvent);
}
Future<void> _onCategoryLoaderEvent(
CategoryLoaderEvent event,
Emitter<CategoryLoaderState> emit,
) {
return event.map(
getCategories: (e) async {
emit(state.copyWith(isLoadingMore: true));
log(
'📱 Loading categories - isActive: ${e.isActive}, forceRemote: ${e.forceRemote}',
);
final result = await _categoryRepository.getCategories(
page: 1,
limit: 50,
isActive: e.isActive,
search: e.search,
forceRemote: e.forceRemote,
);
await result.fold(
(failure) async {
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 50 || 1 >= totalPages;
log(
'✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax',
);
emit(
state.copyWith(
categories: categories,
page: 1,
hasReachedMax: hasReachedMax,
isLoadingMore: false,
failureOptionCategory: none(),
),
);
},
);
},
loadMore: (e) async {
final currentState = state;
// ❌ HAPUS pengecekan is! _Loaded karena state cuma 1 class doang
if (currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
log(
'⏹️ Load more blocked - hasReachedMax: ${currentState.hasReachedMax}, isLoadingMore: $_isLoadingMore',
);
return;
}
_isLoadingMore = true;
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.page + 1; // ✅ Ganti currentPage jadi page
log('📄 Loading more categories - page: $nextPage');
try {
final result = await _categoryRepository.getCategories(
page: nextPage,
limit: 10,
isActive: true,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more categories: $failure');
emit(currentState.copyWith(isLoadingMore: false));
},
(response) async {
final newCategories = response.categories;
final totalPages = response.totalPages;
// Prevent duplicate categories
final currentCategoryIds = currentState.categories
.map((c) => c.id)
.toSet();
final filteredNewCategories = newCategories
.where(
(category) => !currentCategoryIds.contains(category.id),
)
.toList();
final allCategories = List<Category>.from(currentState.categories)
..addAll(filteredNewCategories);
final hasReachedMax =
newCategories.length < 10 || nextPage >= totalPages;
log(
'✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}',
);
emit(
currentState.copyWith(
categories: allCategories,
hasReachedMax: hasReachedMax,
page: nextPage, // ✅ Update page
isLoadingMore: false,
),
);
},
);
} catch (e) {
log('❌ Exception loading more categories: $e');
emit(currentState.copyWith(isLoadingMore: false));
} finally {
_isLoadingMore = false;
}
},
refresh: (e) async {
final currentState = state;
bool isActive = true;
String? searchQuery = currentState.searchQuery;
_isLoadingMore = false;
_searchDebounce?.cancel();
log('🔄 Refreshing categories');
// Clear local cache
_categoryRepository.clearCache();
add(
CategoryLoaderEvent.getCategories(
isActive: isActive,
search: searchQuery,
forceRemote: true, // Force remote refresh
),
);
},
search: (e) async {
// Cancel previous search
_searchDebounce?.cancel();
// Debounce search for better UX
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
emit(state.copyWith(isLoadingMore: true));
_isLoadingMore = false;
log('🔍 Searching categories: "${e.query}"');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 20, // More results for search
isActive: e.isActive,
search: e.query,
);
await result.fold(
(failure) async {
log('❌ Search error: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
log('✅ Search results: ${categories.length} categories found');
emit(
state.copyWith(
categories: categories,
hasReachedMax: hasReachedMax,
page: 1,
isLoadingMore: false,
failureOptionCategory: none(),
searchQuery: e.query,
),
);
},
);
});
},
syncAll: (e) async {
emit(state.copyWith(isLoadingMore: true));
log('🔄 Starting full category sync...');
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
log('❌ Sync failed: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
// After sync error, try to load local data
Timer(Duration(seconds: 2), () {
add(const CategoryLoaderEvent.getCategories());
});
},
(successMessage) async {
log('✅ Sync completed: $successMessage');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: none(),
),
);
// After successful sync, load the updated data
Timer(Duration(seconds: 1), () {
add(const CategoryLoaderEvent.getCategories());
});
},
);
},
getAllCategories: (e) async {
try {
log('📋 Loading all categories for dropdown...');
// final categories = await _categoryRepository.getAllCategories();
// emit(
// state.copyWith(
// categories: categories,
// isLoadingMore: false,
// failureOptionCategory: none(),
// ),
// );
// log('✅ All categories loaded: ${categories.length}');
} catch (e) {
log('❌ Error loading all categories: $e');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(
CategoryFailure.dynamicErrorMessage(
'Gagal memuat semua kategori: $e',
),
),
),
);
}
},
getDatabaseStats: (e) async {
try {
final stats = await _categoryRepository.getDatabaseStats();
log('📊 Category database stats retrieved: $stats');
// You can emit a special state here if needed for UI updates
// For now, just log the stats
} catch (e) {
log('❌ Error getting category database stats: $e');
}
},
clearCache: (e) async {
log('🧹 Manually clearing category cache');
_categoryRepository.clearCache();
// Refresh current data after cache clear
add(const CategoryLoaderEvent.refresh());
},
);
}
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderEvent with _$CategoryLoaderEvent {
const factory CategoryLoaderEvent.getCategories({
@Default(true) bool isActive,
String? search,
@Default(false) bool forceRemote,
}) = _GetCategories;
const factory CategoryLoaderEvent.loadMore() = _LoadMore;
const factory CategoryLoaderEvent.refresh() = _Refresh;
const factory CategoryLoaderEvent.search({
required String query,
@Default(true) bool isActive,
}) = _Search;
const factory CategoryLoaderEvent.syncAll() = _SyncAll;
const factory CategoryLoaderEvent.getAllCategories() = _GetAllCategories;
const factory CategoryLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
const factory CategoryLoaderEvent.clearCache() = _ClearCache;
}
@@ -0,0 +1,16 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderState with _$CategoryLoaderState {
factory CategoryLoaderState({
required List<Category> categories,
required Option<CategoryFailure> failureOptionCategory,
@Default(false) bool hasReachedMax,
@Default(1) int page,
@Default(false) bool isLoadingMore,
String? searchQuery,
}) = _CategoryLoaderState;
factory CategoryLoaderState.initial() =>
CategoryLoaderState(categories: [], failureOptionCategory: none());
}
@@ -0,0 +1,84 @@
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/outlet/outlet.dart';
part 'outlet_loader_event.dart';
part 'outlet_loader_state.dart';
part 'outlet_loader_bloc.freezed.dart';
@injectable
class OutletLoaderBloc extends Bloc<OutletLoaderEvent, OutletLoaderState> {
final IOutletRepository _outletRepository;
OutletLoaderBloc(this._outletRepository)
: super(OutletLoaderState.initial()) {
on<OutletLoaderEvent>(_onOutletLoaderEvent);
}
Future<void> _onOutletLoaderEvent(
OutletLoaderEvent event,
Emitter<OutletLoaderState> emit,
) {
return event.map(
fetched: (e) async {
var newState = state;
if (e.isRefresh) {
newState = state.copyWith(isFetching: true);
emit(newState);
}
newState = await _mapFetchedToState(state, isRefresh: e.isRefresh);
emit(newState);
},
);
}
Future<OutletLoaderState> _mapFetchedToState(
OutletLoaderState state, {
bool isRefresh = false,
}) async {
state = state.copyWith(isFetching: false);
if (state.hasReachedMax && state.outlets.isNotEmpty && !isRefresh) {
return state;
}
if (isRefresh) {
state = state.copyWith(
page: 1,
failureOptionOutlet: none(),
hasReachedMax: false,
outlets: [],
);
}
final failureOrOutlet = await _outletRepository.getOutlets(
page: state.page,
);
state = failureOrOutlet.fold(
(f) {
if (state.outlets.isNotEmpty) {
return state.copyWith(hasReachedMax: true);
}
return state.copyWith(failureOptionOutlet: optionOf(f));
},
(outlets) {
return state.copyWith(
outlets: List.from(state.outlets)..addAll(outlets),
failureOptionOutlet: none(),
page: state.page + 1,
hasReachedMax: outlets.length < 10,
);
},
);
return state;
}
}
@@ -0,0 +1,479 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'outlet_loader_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$OutletLoaderEvent {
bool get isRefresh => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
/// Create a copy of OutletLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$OutletLoaderEventCopyWith<OutletLoaderEvent> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $OutletLoaderEventCopyWith<$Res> {
factory $OutletLoaderEventCopyWith(
OutletLoaderEvent value,
$Res Function(OutletLoaderEvent) then,
) = _$OutletLoaderEventCopyWithImpl<$Res, OutletLoaderEvent>;
@useResult
$Res call({bool isRefresh});
}
/// @nodoc
class _$OutletLoaderEventCopyWithImpl<$Res, $Val extends OutletLoaderEvent>
implements $OutletLoaderEventCopyWith<$Res> {
_$OutletLoaderEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of OutletLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? isRefresh = null}) {
return _then(
_value.copyWith(
isRefresh: null == isRefresh
? _value.isRefresh
: isRefresh // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$FetchedImplCopyWith<$Res>
implements $OutletLoaderEventCopyWith<$Res> {
factory _$$FetchedImplCopyWith(
_$FetchedImpl value,
$Res Function(_$FetchedImpl) then,
) = __$$FetchedImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isRefresh});
}
/// @nodoc
class __$$FetchedImplCopyWithImpl<$Res>
extends _$OutletLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
implements _$$FetchedImplCopyWith<$Res> {
__$$FetchedImplCopyWithImpl(
_$FetchedImpl _value,
$Res Function(_$FetchedImpl) _then,
) : super(_value, _then);
/// Create a copy of OutletLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? isRefresh = null}) {
return _then(
_$FetchedImpl(
isRefresh: null == isRefresh
? _value.isRefresh
: isRefresh // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$FetchedImpl implements _Fetched {
const _$FetchedImpl({this.isRefresh = false});
@override
@JsonKey()
final bool isRefresh;
@override
String toString() {
return 'OutletLoaderEvent.fetched(isRefresh: $isRefresh)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$FetchedImpl &&
(identical(other.isRefresh, isRefresh) ||
other.isRefresh == isRefresh));
}
@override
int get hashCode => Object.hash(runtimeType, isRefresh);
/// Create a copy of OutletLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
__$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(bool isRefresh) fetched,
}) {
return fetched(isRefresh);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(bool isRefresh)? fetched,
}) {
return fetched?.call(isRefresh);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(bool isRefresh)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(isRefresh);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Fetched value) fetched,
}) {
return fetched(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Fetched value)? fetched,
required TResult orElse(),
}) {
if (fetched != null) {
return fetched(this);
}
return orElse();
}
}
abstract class _Fetched implements OutletLoaderEvent {
const factory _Fetched({final bool isRefresh}) = _$FetchedImpl;
@override
bool get isRefresh;
/// Create a copy of OutletLoaderEvent
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$OutletLoaderState {
List<Outlet> get outlets => throw _privateConstructorUsedError;
Option<OutletFailure> get failureOptionOutlet =>
throw _privateConstructorUsedError;
bool get isFetching => throw _privateConstructorUsedError;
bool get hasReachedMax => throw _privateConstructorUsedError;
int get page => throw _privateConstructorUsedError;
/// Create a copy of OutletLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$OutletLoaderStateCopyWith<OutletLoaderState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $OutletLoaderStateCopyWith<$Res> {
factory $OutletLoaderStateCopyWith(
OutletLoaderState value,
$Res Function(OutletLoaderState) then,
) = _$OutletLoaderStateCopyWithImpl<$Res, OutletLoaderState>;
@useResult
$Res call({
List<Outlet> outlets,
Option<OutletFailure> failureOptionOutlet,
bool isFetching,
bool hasReachedMax,
int page,
});
}
/// @nodoc
class _$OutletLoaderStateCopyWithImpl<$Res, $Val extends OutletLoaderState>
implements $OutletLoaderStateCopyWith<$Res> {
_$OutletLoaderStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of OutletLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? outlets = null,
Object? failureOptionOutlet = null,
Object? isFetching = null,
Object? hasReachedMax = null,
Object? page = null,
}) {
return _then(
_value.copyWith(
outlets: null == outlets
? _value.outlets
: outlets // ignore: cast_nullable_to_non_nullable
as List<Outlet>,
failureOptionOutlet: null == failureOptionOutlet
? _value.failureOptionOutlet
: failureOptionOutlet // ignore: cast_nullable_to_non_nullable
as Option<OutletFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
hasReachedMax: null == hasReachedMax
? _value.hasReachedMax
: hasReachedMax // ignore: cast_nullable_to_non_nullable
as bool,
page: null == page
? _value.page
: page // ignore: cast_nullable_to_non_nullable
as int,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$OutletLoaderStateImplCopyWith<$Res>
implements $OutletLoaderStateCopyWith<$Res> {
factory _$$OutletLoaderStateImplCopyWith(
_$OutletLoaderStateImpl value,
$Res Function(_$OutletLoaderStateImpl) then,
) = __$$OutletLoaderStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
List<Outlet> outlets,
Option<OutletFailure> failureOptionOutlet,
bool isFetching,
bool hasReachedMax,
int page,
});
}
/// @nodoc
class __$$OutletLoaderStateImplCopyWithImpl<$Res>
extends _$OutletLoaderStateCopyWithImpl<$Res, _$OutletLoaderStateImpl>
implements _$$OutletLoaderStateImplCopyWith<$Res> {
__$$OutletLoaderStateImplCopyWithImpl(
_$OutletLoaderStateImpl _value,
$Res Function(_$OutletLoaderStateImpl) _then,
) : super(_value, _then);
/// Create a copy of OutletLoaderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? outlets = null,
Object? failureOptionOutlet = null,
Object? isFetching = null,
Object? hasReachedMax = null,
Object? page = null,
}) {
return _then(
_$OutletLoaderStateImpl(
outlets: null == outlets
? _value._outlets
: outlets // ignore: cast_nullable_to_non_nullable
as List<Outlet>,
failureOptionOutlet: null == failureOptionOutlet
? _value.failureOptionOutlet
: failureOptionOutlet // ignore: cast_nullable_to_non_nullable
as Option<OutletFailure>,
isFetching: null == isFetching
? _value.isFetching
: isFetching // ignore: cast_nullable_to_non_nullable
as bool,
hasReachedMax: null == hasReachedMax
? _value.hasReachedMax
: hasReachedMax // ignore: cast_nullable_to_non_nullable
as bool,
page: null == page
? _value.page
: page // ignore: cast_nullable_to_non_nullable
as int,
),
);
}
}
/// @nodoc
class _$OutletLoaderStateImpl implements _OutletLoaderState {
const _$OutletLoaderStateImpl({
required final List<Outlet> outlets,
required this.failureOptionOutlet,
this.isFetching = false,
this.hasReachedMax = false,
this.page = 1,
}) : _outlets = outlets;
final List<Outlet> _outlets;
@override
List<Outlet> get outlets {
if (_outlets is EqualUnmodifiableListView) return _outlets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_outlets);
}
@override
final Option<OutletFailure> failureOptionOutlet;
@override
@JsonKey()
final bool isFetching;
@override
@JsonKey()
final bool hasReachedMax;
@override
@JsonKey()
final int page;
@override
String toString() {
return 'OutletLoaderState(outlets: $outlets, failureOptionOutlet: $failureOptionOutlet, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$OutletLoaderStateImpl &&
const DeepCollectionEquality().equals(other._outlets, _outlets) &&
(identical(other.failureOptionOutlet, failureOptionOutlet) ||
other.failureOptionOutlet == failureOptionOutlet) &&
(identical(other.isFetching, isFetching) ||
other.isFetching == isFetching) &&
(identical(other.hasReachedMax, hasReachedMax) ||
other.hasReachedMax == hasReachedMax) &&
(identical(other.page, page) || other.page == page));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_outlets),
failureOptionOutlet,
isFetching,
hasReachedMax,
page,
);
/// Create a copy of OutletLoaderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$OutletLoaderStateImplCopyWith<_$OutletLoaderStateImpl> get copyWith =>
__$$OutletLoaderStateImplCopyWithImpl<_$OutletLoaderStateImpl>(
this,
_$identity,
);
}
abstract class _OutletLoaderState implements OutletLoaderState {
const factory _OutletLoaderState({
required final List<Outlet> outlets,
required final Option<OutletFailure> failureOptionOutlet,
final bool isFetching,
final bool hasReachedMax,
final int page,
}) = _$OutletLoaderStateImpl;
@override
List<Outlet> get outlets;
@override
Option<OutletFailure> get failureOptionOutlet;
@override
bool get isFetching;
@override
bool get hasReachedMax;
@override
int get page;
/// Create a copy of OutletLoaderState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$OutletLoaderStateImplCopyWith<_$OutletLoaderStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,7 @@
part of 'outlet_loader_bloc.dart';
@freezed
class OutletLoaderEvent with _$OutletLoaderEvent {
const factory OutletLoaderEvent.fetched({@Default(false) bool isRefresh}) =
_Fetched;
}
@@ -0,0 +1,15 @@
part of 'outlet_loader_bloc.dart';
@freezed
class OutletLoaderState with _$OutletLoaderState {
const factory OutletLoaderState({
required List<Outlet> outlets,
required Option<OutletFailure> failureOptionOutlet,
@Default(false) bool isFetching,
@Default(false) bool hasReachedMax,
@Default(1) int page,
}) = _OutletLoaderState;
factory OutletLoaderState.initial() =>
OutletLoaderState(outlets: [], failureOptionOutlet: none());
}
@@ -0,0 +1,345 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/product/product.dart';
part 'product_loader_event.dart';
part 'product_loader_state.dart';
part 'product_loader_bloc.freezed.dart';
@injectable
class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
final IProductRepository _productRepository;
Timer? _loadMoreDebounce;
Timer? _searchDebounce;
ProductLoaderBloc(this._productRepository)
: super(ProductLoaderState.initial()) {
on<ProductLoaderEvent>(_onProductLoaderEvent);
}
Future<void> _onProductLoaderEvent(
ProductLoaderEvent event,
Emitter<ProductLoaderState> emit,
) {
return event.map(
getProduct: (e) async {
emit(state.copyWith(isLoadingMore: true));
log(
'📱 Loading local products - categoryId: ${e.categoryId}, search: ${e.search}',
);
// Pastikan database lokal sudah siap
final isReady = await _productRepository.isLocalDatabaseReady();
if (!isReady) {
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(
ProductFailure.dynamicErrorMessage(
'Database lokal belum siap. Silakan lakukan sinkronisasi data terlebih dahulu.',
),
),
),
);
return;
}
final result = await _productRepository.getProducts(
page: 1,
limit: 10,
categoryId: e.categoryId,
search: e.search,
);
await result.fold(
(failure) async {
log('❌ Error loading local products: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(failure),
),
);
},
(response) async {
final products = response.products;
final totalPages = response.totalPages;
final hasReachedMax = products.length < 10 || 1 >= totalPages;
log(
'✅ Local products loaded: ${products.length}, hasReachedMax: $hasReachedMax, totalPages: $totalPages',
);
emit(
state.copyWith(
products: products,
page: 1,
hasReachedMax: hasReachedMax,
isLoadingMore: false,
failureOptionProduct: none(),
categoryId: e.categoryId,
searchQuery: e.search,
),
);
},
);
},
loadMore: (e) async {
final currentState = state;
// Cegah double load
if (currentState.isLoadingMore || currentState.hasReachedMax) {
log(
'⏹️ Load more blocked - isLoadingMore: ${currentState.isLoadingMore}, hasReachedMax: ${currentState.hasReachedMax}',
);
return;
}
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.page + 1;
log('📄 Loading more local products - page: $nextPage');
try {
final result = await _productRepository.getProducts(
page: nextPage,
limit: 10,
categoryId: currentState.categoryId,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more local products: $failure');
emit(
currentState.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(failure),
),
);
},
(response) async {
final newProducts = response.products;
final totalPages = response.totalPages;
// Hindari duplikat produk
final currentProductIds = currentState.products
.map((p) => p.id)
.toSet();
final filteredNewProducts = newProducts
.where((product) => !currentProductIds.contains(product.id))
.toList();
final allProducts = [
...currentState.products,
...filteredNewProducts,
];
final hasReachedMax =
filteredNewProducts.length < 10 || nextPage >= totalPages;
log(
'✅ More local products loaded: ${filteredNewProducts.length} new, total: ${allProducts.length}, hasReachedMax: $hasReachedMax',
);
emit(
currentState.copyWith(
products: allProducts,
page: nextPage,
hasReachedMax: hasReachedMax,
isLoadingMore: false,
failureOptionProduct: none(),
),
);
},
);
} catch (e) {
log('❌ Exception loading more local products: $e');
emit(
currentState.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(
ProductFailure.dynamicErrorMessage(
'Gagal memuat produk tambahan: $e',
),
),
),
);
}
},
refresh: (e) async {
final categoryId = state.categoryId;
final searchQuery = state.searchQuery;
_loadMoreDebounce?.cancel();
_searchDebounce?.cancel();
log(
'🔄 Refreshing local products - categoryId: $categoryId, search: $searchQuery',
);
emit(state.copyWith(isLoadingMore: true));
try {
_productRepository.clearCache();
final result = await _productRepository.refreshProducts(
categoryId: categoryId,
search: searchQuery,
);
await result.fold(
(failure) async {
log('❌ Failed to refresh local products: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(failure),
),
);
},
(response) async {
final products = response.products;
final totalPages = response.totalPages;
final hasReachedMax = products.length < 10 || 1 >= totalPages;
log('✅ Refreshed local products: ${products.length}');
emit(
state.copyWith(
products: products,
hasReachedMax: hasReachedMax,
page: 1,
isLoadingMore: false,
failureOptionProduct: none(),
categoryId: categoryId,
searchQuery: searchQuery,
),
);
},
);
} catch (e) {
log('❌ Exception refreshing local products: $e');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(
ProductFailure.dynamicErrorMessage(e.toString()),
),
),
);
} finally {}
},
searchProduct: (e) async {
_searchDebounce?.cancel();
// Debounce ringan agar UX lebih halus
_searchDebounce = Timer(const Duration(milliseconds: 150), () async {
emit(state.copyWith(isLoadingMore: true));
log('🔍 Local search: "${e.query}"');
try {
final result = await _productRepository.getProducts(
page: 1,
limit: 20, // lebih banyak hasil untuk pencarian
categoryId: e.categoryId,
search: e.query,
);
await result.fold(
(failure) async {
log('❌ Local search error: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(failure),
),
);
},
(response) async {
final products = response.products;
final totalPages = response.totalPages;
final hasReachedMax = products.length < 20 || 1 >= totalPages;
log(
'✅ Local search results: ${products.length} products found',
);
emit(
state.copyWith(
products: products,
hasReachedMax: hasReachedMax,
page: 1,
isLoadingMore: false,
categoryId: e.categoryId,
searchQuery: e.query,
failureOptionProduct: none(),
),
);
},
);
} catch (e) {
log('❌ Exception during local search: $e');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionProduct: optionOf(
ProductFailure.dynamicErrorMessage(e.toString()),
),
),
);
}
});
},
getDatabaseStats: (e) async {
log('📊 Getting local database stats...');
try {
final result = await _productRepository.getDatabaseStats();
await result.fold(
(failure) async {
log('❌ Failed to get database stats: $failure');
emit(state.copyWith(failureOptionProduct: optionOf(failure)));
},
(stats) async {
log('✅ Local database stats retrieved: $stats');
// Jika UI kamu perlu tampilkan, bisa simpan ke state, misalnya:
// emit(state.copyWith(databaseStats: some(stats)));
// Tapi kalau hanya untuk log/debug, tidak perlu ubah state
},
);
} catch (e, s) {
log(
'❌ Exception while getting database stats: $e',
error: e,
stackTrace: s,
);
emit(
state.copyWith(
failureOptionProduct: optionOf(
ProductFailure.dynamicErrorMessage(e.toString()),
),
),
);
}
},
clearCache: (e) async {
log('🧹 Manually clearing local cache');
_productRepository.clearCache();
// Refresh current data after cache clear
add(const ProductLoaderEvent.refresh());
},
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
part of 'product_loader_bloc.dart';
@freezed
class ProductLoaderEvent with _$ProductLoaderEvent {
const factory ProductLoaderEvent.getProduct({
String? categoryId,
String? search, // Added search parameter
bool? forceRefresh, // Kept for compatibility but ignored
}) = _GetProduct;
const factory ProductLoaderEvent.loadMore({
String? categoryId,
String? search,
}) = _LoadMore;
const factory ProductLoaderEvent.refresh() = _Refresh;
const factory ProductLoaderEvent.searchProduct({
String? query,
String? categoryId,
}) = _SearchProduct;
const factory ProductLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
const factory ProductLoaderEvent.clearCache() = _ClearCache;
}
@@ -0,0 +1,17 @@
part of 'product_loader_bloc.dart';
@freezed
class ProductLoaderState with _$ProductLoaderState {
factory ProductLoaderState({
required List<Product> products,
required Option<ProductFailure> failureOptionProduct,
@Default(false) bool hasReachedMax,
@Default(1) int page,
@Default(false) bool isLoadingMore,
String? searchQuery,
String? categoryId,
}) = _ProductLoaderState;
factory ProductLoaderState.initial() =>
ProductLoaderState(products: [], failureOptionProduct: none());
}
+323
View File
@@ -0,0 +1,323 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';
import '../../domain/category/category.dart';
import '../../domain/product/product.dart';
part 'sync_event.dart';
part 'sync_state.dart';
part 'sync_bloc.freezed.dart';
enum SyncStep { categories, products, variants, completed }
class SyncStats {
final int totalProducts;
final int totalCategories;
final int totalVariants;
final double databaseSizeMB;
SyncStats({
required this.totalProducts,
required this.totalCategories,
required this.totalVariants,
required this.databaseSizeMB,
});
}
@injectable
class SyncBloc extends Bloc<SyncEvent, SyncState> {
final IProductRepository _productRepository;
final ICategoryRepository _categoryRepository;
Timer? _progressTimer;
bool _isCancelled = false;
SyncBloc(this._productRepository, this._categoryRepository)
: super(SyncState.initial()) {
on<SyncEvent>(_onSyncEvent);
}
Future<void> _onSyncEvent(SyncEvent event, Emitter<SyncState> emit) {
return event.map(
startSync: (e) async {
log('🔄 Starting full data sync (categories + products)...');
_isCancelled = false;
emit(
state.copyWith(
isSyncing: true,
currentStep: SyncStep.categories,
progress: 0.05,
message: 'Membersihkan data lama...',
errorMessage: null,
),
);
try {
// Step 1: Clear existing local data
await _productRepository.clearAllProducts();
await _categoryRepository.clearAllCategories();
if (_isCancelled) return;
// Step 2: Sync categories first
await _syncCategories(emit);
if (_isCancelled) return;
// Step 3: Sync products
await _syncProducts(emit);
if (_isCancelled) return;
// Step 4: Final stats
emit(
state.copyWith(
currentStep: SyncStep.completed,
progress: 0.95,
message: 'Menyelesaikan sinkronisasi...',
),
);
final stats = await _generateSyncStats();
emit(
state.copyWith(
isSyncing: false,
stats: stats,
progress: 1.0,
message: 'Sinkronisasi selesai ✅',
),
);
log('✅ Full sync completed successfully');
} catch (e) {
log('❌ Sync failed: $e');
emit(
state.copyWith(
isSyncing: false,
errorMessage: 'Gagal sinkronisasi: $e',
message: 'Sinkronisasi gagal ❌',
),
);
}
},
cancelSync: (e) async {
log('⏹️ Cancelling sync...');
_isCancelled = true;
_progressTimer?.cancel();
emit(SyncState.initial());
},
);
}
Future<void> _syncCategories(Emitter<SyncState> emit) async {
log('📁 Syncing categories...');
emit(
state.copyWith(
isSyncing: true,
currentStep: SyncStep.categories,
progress: 0.1,
message: 'Mengunduh kategori...',
errorMessage: null,
),
);
try {
// Gunakan CategoryRepository sync method
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
throw Exception('Gagal sync kategori: $failure');
},
(successMessage) async {
log('✅ Categories sync completed: $successMessage');
emit(
state.copyWith(
currentStep: SyncStep.categories,
progress: 0.2,
message: 'Kategori berhasil diunduh ✅',
),
);
},
);
} catch (e) {
log('❌ Category sync failed: $e');
emit(
state.copyWith(
isSyncing: false,
errorMessage: 'Gagal sync kategori: $e',
message: 'Sinkronisasi kategori gagal ❌',
),
);
rethrow; // penting agar _onStartSync tahu kalau gagal
}
}
Future<void> _syncProducts(Emitter<SyncState> emit) async {
log('📦 Syncing products...');
int page = 1;
int totalSynced = 0;
int? totalCount;
int? totalPages;
bool shouldContinue = true;
while (!_isCancelled && shouldContinue) {
// Hitung progress dinamis (kategori 0.00.2, produk 0.20.9)
double progress = 0.2;
if (totalCount != null && totalCount! > 0) {
progress = 0.2 + (totalSynced / totalCount!) * 0.7;
}
emit(
state.copyWith(
isSyncing: true,
currentStep: SyncStep.products,
progress: progress,
message: totalCount != null
? 'Mengunduh produk... ($totalSynced dari $totalCount)'
: 'Mengunduh produk... ($totalSynced produk)',
errorMessage: null,
),
);
final result = await _productRepository.getProducts(
page: page,
limit: 50, // ambil batch besar biar cepat
);
await result.fold(
(failure) async {
emit(
state.copyWith(
isSyncing: false,
errorMessage: 'Gagal sync produk: $failure',
message: 'Sinkronisasi produk gagal ❌',
),
);
throw Exception(failure);
},
(response) async {
final products = response.products;
final responseData = response;
// Ambil total count & total page dari respons pertama
if (page == 1) {
totalCount = responseData.totalCount;
totalPages = responseData.totalPages;
log('📊 Total products to sync: $totalCount ($totalPages pages)');
}
if (products.isEmpty) {
shouldContinue = false;
return;
}
// Simpan batch produk ke local DB
await _productRepository.saveProductsBatch(products);
totalSynced += products.length;
page++;
log(
'📦 Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)',
);
// Cek apakah sudah selesai sync
if (totalPages != null && page > totalPages!) {
shouldContinue = false;
return;
}
// Fallback jika pagination info tidak lengkap
if (products.length < 50) {
shouldContinue = false;
return;
}
// Tambahkan delay kecil agar tidak overload server
await Future.delayed(const Duration(milliseconds: 100));
},
);
}
// Selesai sync produk
emit(
state.copyWith(
progress: 0.9,
message: 'Sinkronisasi produk selesai ✅ ($totalSynced total)',
),
);
}
Future<SyncStats> _generateSyncStats() async {
try {
log('📊 Generating sync statistics via repository...');
// Jalankan kedua query secara paralel untuk efisiensi
final results = await Future.wait([
_productRepository.getDatabaseStats(),
_categoryRepository.getDatabaseStats(),
]);
// Default kosong
Map<String, dynamic> productStats = {};
Map<String, dynamic> categoryStats = {};
// Ambil hasil product stats
await results[0].fold(
(failure) async {
log('⚠️ Failed to get product stats: $failure');
},
(data) async {
productStats = data;
},
);
// Ambil hasil category stats
await results[1].fold(
(failure) async {
log('⚠️ Failed to get category stats: $failure');
},
(data) async {
categoryStats = data;
},
);
// Bangun objek SyncStats akhir
final stats = SyncStats(
totalProducts: productStats['total_products'] ?? 0,
totalCategories: categoryStats['total_categories'] ?? 0,
totalVariants: productStats['total_variants'] ?? 0,
databaseSizeMB:
((productStats['database_size_mb'] ?? 0.0) as num).toDouble() +
((categoryStats['database_size_mb'] ?? 0.0) as num).toDouble(),
);
log('✅ Sync stats generated: $stats');
return stats;
} catch (e, stack) {
log('❌ Error generating sync stats: $e\n$stack');
return SyncStats(
totalProducts: 0,
totalCategories: 0,
totalVariants: 0,
databaseSizeMB: 0.0,
);
}
}
@override
Future<void> close() {
_progressTimer?.cancel();
return super.close();
}
}
+542
View File
@@ -0,0 +1,542 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'sync_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$SyncEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SyncEventCopyWith<$Res> {
factory $SyncEventCopyWith(SyncEvent value, $Res Function(SyncEvent) then) =
_$SyncEventCopyWithImpl<$Res, SyncEvent>;
}
/// @nodoc
class _$SyncEventCopyWithImpl<$Res, $Val extends SyncEvent>
implements $SyncEventCopyWith<$Res> {
_$SyncEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$StartSyncImplCopyWith<$Res> {
factory _$$StartSyncImplCopyWith(
_$StartSyncImpl value,
$Res Function(_$StartSyncImpl) then,
) = __$$StartSyncImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$StartSyncImplCopyWithImpl<$Res>
extends _$SyncEventCopyWithImpl<$Res, _$StartSyncImpl>
implements _$$StartSyncImplCopyWith<$Res> {
__$$StartSyncImplCopyWithImpl(
_$StartSyncImpl _value,
$Res Function(_$StartSyncImpl) _then,
) : super(_value, _then);
/// Create a copy of SyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$StartSyncImpl implements _StartSync {
const _$StartSyncImpl();
@override
String toString() {
return 'SyncEvent.startSync()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$StartSyncImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) {
return startSync();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) {
return startSync?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) {
if (startSync != null) {
return startSync();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) {
return startSync(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) {
return startSync?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) {
if (startSync != null) {
return startSync(this);
}
return orElse();
}
}
abstract class _StartSync implements SyncEvent {
const factory _StartSync() = _$StartSyncImpl;
}
/// @nodoc
abstract class _$$CancelSyncImplCopyWith<$Res> {
factory _$$CancelSyncImplCopyWith(
_$CancelSyncImpl value,
$Res Function(_$CancelSyncImpl) then,
) = __$$CancelSyncImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$CancelSyncImplCopyWithImpl<$Res>
extends _$SyncEventCopyWithImpl<$Res, _$CancelSyncImpl>
implements _$$CancelSyncImplCopyWith<$Res> {
__$$CancelSyncImplCopyWithImpl(
_$CancelSyncImpl _value,
$Res Function(_$CancelSyncImpl) _then,
) : super(_value, _then);
/// Create a copy of SyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$CancelSyncImpl implements _CancelSync {
const _$CancelSyncImpl();
@override
String toString() {
return 'SyncEvent.cancelSync()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$CancelSyncImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) {
return cancelSync();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) {
return cancelSync?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) {
if (cancelSync != null) {
return cancelSync();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) {
return cancelSync(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) {
return cancelSync?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) {
if (cancelSync != null) {
return cancelSync(this);
}
return orElse();
}
}
abstract class _CancelSync implements SyncEvent {
const factory _CancelSync() = _$CancelSyncImpl;
}
/// @nodoc
mixin _$SyncState {
bool get isSyncing => throw _privateConstructorUsedError;
double get progress => throw _privateConstructorUsedError;
SyncStep? get currentStep => throw _privateConstructorUsedError;
String? get message => throw _privateConstructorUsedError;
SyncStats? get stats => throw _privateConstructorUsedError;
String? get errorMessage => throw _privateConstructorUsedError;
/// Create a copy of SyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SyncStateCopyWith<SyncState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SyncStateCopyWith<$Res> {
factory $SyncStateCopyWith(SyncState value, $Res Function(SyncState) then) =
_$SyncStateCopyWithImpl<$Res, SyncState>;
@useResult
$Res call({
bool isSyncing,
double progress,
SyncStep? currentStep,
String? message,
SyncStats? stats,
String? errorMessage,
});
}
/// @nodoc
class _$SyncStateCopyWithImpl<$Res, $Val extends SyncState>
implements $SyncStateCopyWith<$Res> {
_$SyncStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isSyncing = null,
Object? progress = null,
Object? currentStep = freezed,
Object? message = freezed,
Object? stats = freezed,
Object? errorMessage = freezed,
}) {
return _then(
_value.copyWith(
isSyncing: null == isSyncing
? _value.isSyncing
: isSyncing // ignore: cast_nullable_to_non_nullable
as bool,
progress: null == progress
? _value.progress
: progress // ignore: cast_nullable_to_non_nullable
as double,
currentStep: freezed == currentStep
? _value.currentStep
: currentStep // ignore: cast_nullable_to_non_nullable
as SyncStep?,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
stats: freezed == stats
? _value.stats
: stats // ignore: cast_nullable_to_non_nullable
as SyncStats?,
errorMessage: freezed == errorMessage
? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$SyncStateImplCopyWith<$Res>
implements $SyncStateCopyWith<$Res> {
factory _$$SyncStateImplCopyWith(
_$SyncStateImpl value,
$Res Function(_$SyncStateImpl) then,
) = __$$SyncStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
bool isSyncing,
double progress,
SyncStep? currentStep,
String? message,
SyncStats? stats,
String? errorMessage,
});
}
/// @nodoc
class __$$SyncStateImplCopyWithImpl<$Res>
extends _$SyncStateCopyWithImpl<$Res, _$SyncStateImpl>
implements _$$SyncStateImplCopyWith<$Res> {
__$$SyncStateImplCopyWithImpl(
_$SyncStateImpl _value,
$Res Function(_$SyncStateImpl) _then,
) : super(_value, _then);
/// Create a copy of SyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isSyncing = null,
Object? progress = null,
Object? currentStep = freezed,
Object? message = freezed,
Object? stats = freezed,
Object? errorMessage = freezed,
}) {
return _then(
_$SyncStateImpl(
isSyncing: null == isSyncing
? _value.isSyncing
: isSyncing // ignore: cast_nullable_to_non_nullable
as bool,
progress: null == progress
? _value.progress
: progress // ignore: cast_nullable_to_non_nullable
as double,
currentStep: freezed == currentStep
? _value.currentStep
: currentStep // ignore: cast_nullable_to_non_nullable
as SyncStep?,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
stats: freezed == stats
? _value.stats
: stats // ignore: cast_nullable_to_non_nullable
as SyncStats?,
errorMessage: freezed == errorMessage
? _value.errorMessage
: errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
class _$SyncStateImpl implements _SyncState {
const _$SyncStateImpl({
this.isSyncing = false,
this.progress = 0.0,
this.currentStep,
this.message,
this.stats,
this.errorMessage,
});
@override
@JsonKey()
final bool isSyncing;
@override
@JsonKey()
final double progress;
@override
final SyncStep? currentStep;
@override
final String? message;
@override
final SyncStats? stats;
@override
final String? errorMessage;
@override
String toString() {
return 'SyncState(isSyncing: $isSyncing, progress: $progress, currentStep: $currentStep, message: $message, stats: $stats, errorMessage: $errorMessage)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncStateImpl &&
(identical(other.isSyncing, isSyncing) ||
other.isSyncing == isSyncing) &&
(identical(other.progress, progress) ||
other.progress == progress) &&
(identical(other.currentStep, currentStep) ||
other.currentStep == currentStep) &&
(identical(other.message, message) || other.message == message) &&
(identical(other.stats, stats) || other.stats == stats) &&
(identical(other.errorMessage, errorMessage) ||
other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(
runtimeType,
isSyncing,
progress,
currentStep,
message,
stats,
errorMessage,
);
/// Create a copy of SyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SyncStateImplCopyWith<_$SyncStateImpl> get copyWith =>
__$$SyncStateImplCopyWithImpl<_$SyncStateImpl>(this, _$identity);
}
abstract class _SyncState implements SyncState {
const factory _SyncState({
final bool isSyncing,
final double progress,
final SyncStep? currentStep,
final String? message,
final SyncStats? stats,
final String? errorMessage,
}) = _$SyncStateImpl;
@override
bool get isSyncing;
@override
double get progress;
@override
SyncStep? get currentStep;
@override
String? get message;
@override
SyncStats? get stats;
@override
String? get errorMessage;
/// Create a copy of SyncState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SyncStateImplCopyWith<_$SyncStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+7
View File
@@ -0,0 +1,7 @@
part of 'sync_bloc.dart';
@freezed
class SyncEvent with _$SyncEvent {
const factory SyncEvent.startSync() = _StartSync;
const factory SyncEvent.cancelSync() = _CancelSync;
}
+15
View File
@@ -0,0 +1,15 @@
part of 'sync_bloc.dart';
@freezed
class SyncState with _$SyncState {
const factory SyncState({
@Default(false) bool isSyncing,
@Default(0.0) double progress,
SyncStep? currentStep,
String? message,
SyncStats? stats,
String? errorMessage,
}) = _SyncState;
factory SyncState.initial() => const SyncState();
}
+243
View File
@@ -0,0 +1,243 @@
// ignore: depend_on_referenced_packages
import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import '../../env.dart';
import 'api_failure.dart';
import 'errors/bad_network_error.dart';
import 'errors/bad_request_error.dart';
import 'errors/connection_timeout_error.dart';
import 'errors/internal_server_error.dart';
import 'errors/not_found_error.dart';
import 'errors/unauthorized_error.dart';
import 'interceptors/bad_network_interceptor.dart';
import 'interceptors/bad_request_interceptor.dart';
import 'interceptors/connection_timeout_interceptor.dart';
import 'interceptors/crashlytic_interceptor.dart';
import 'interceptors/internal_server_interceptor.dart';
import 'interceptors/not_found_interceptor.dart';
import 'interceptors/unauthorized_interceptor.dart';
@lazySingleton
class ApiClient {
final Dio _dio;
final Env _env;
ApiClient(this._dio, this._env) {
_dio.options.baseUrl = _env.baseUrl;
_dio.options.connectTimeout = const Duration(seconds: 20);
_dio.options.validateStatus = (status) =>
status != null && status >= 200 && status < 500;
_dio.interceptors.add(BadNetworkErrorInterceptor());
_dio.interceptors.add(BadRequestErrorInterceptor());
_dio.interceptors.add(InternalServerErrorInterceptor());
_dio.interceptors.add(NotFoundErrorInterceptor());
_dio.interceptors.add(UnauthorizedInterceptor());
_dio.interceptors.add(ConnectionTimeoutErrorInterceptor());
_dio.interceptors.add(CrashlyticsInterceptor());
if (kDebugMode) {
_dio.interceptors.add(
AwesomeDioInterceptor(
logResponseHeaders: false,
logRequestTimeout: false,
logRequestHeaders: true,
),
);
}
}
Future<Response> post(
String path, {
dynamic data,
Map<String, dynamic>? headers,
Map<String, dynamic>? params,
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
}) async {
try {
return await _dio.post(
path,
data: data,
options: Options(
headers: headers,
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
),
queryParameters: params,
);
} on UnauthorizedError catch (e) {
throw ApiFailure.unauthorized(e.messageError);
} on InternalServerError {
throw const ApiFailure.internalServerError();
} on BadNetworkError {
throw const ApiFailure.connectionError();
} on BadRequestError catch (e) {
throw ApiFailure.badRequest(e.messageError);
} on NotFoundError catch (e) {
throw ApiFailure.notFound(e.messageError);
} on ConnectionTimeoutError {
throw const ApiFailure.connectionTimeout();
} on DioException catch (e) {
var errorMessage =
e.response?.data['message'] ?? e.response?.statusMessage ?? e.error;
if (errorMessage.toString().contains('Connection reset')) {
errorMessage = 'Connection reset';
}
throw ApiFailure.serverError(
statusCode: e.response?.statusCode ?? 0,
errorMessage: errorMessage.toString(),
);
} catch (e, s) {
throw ApiFailure.unexpectedError(errorMessage: e, stackTrace: s);
}
}
Future<Response> get(
String path, {
Map<String, dynamic>? headers,
Map<String, dynamic>? params,
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
}) async {
try {
return await _dio.get(
path,
options: Options(
headers: headers,
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
),
queryParameters: params,
);
} on UnauthorizedError catch (e) {
throw ApiFailure.unauthorized(e.messageError);
} on InternalServerError {
throw const ApiFailure.internalServerError();
} on BadNetworkError {
throw const ApiFailure.connectionError();
} on BadRequestError catch (e) {
throw ApiFailure.badRequest(e.messageError);
} on NotFoundError catch (e) {
throw ApiFailure.notFound(e.messageError);
} on ConnectionTimeoutError {
throw const ApiFailure.connectionTimeout();
} on DioException catch (e) {
var errorMessage =
e.response?.data['message'] ?? e.response?.statusMessage ?? e.error;
if (errorMessage.toString().contains('Connection reset')) {
errorMessage = 'Connection reset';
}
throw ApiFailure.serverError(
statusCode: e.response?.statusCode ?? 0,
errorMessage: errorMessage.toString(),
);
} catch (e, s) {
throw ApiFailure.unexpectedError(errorMessage: e, stackTrace: s);
}
}
Future<Response> put(
String path, {
dynamic data,
Map<String, dynamic>? headers,
Map<String, dynamic>? params,
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
void Function(int count, int total)? onSendProgress,
}) async {
try {
return await _dio.put(
path,
data: data,
options: Options(
headers: headers,
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
),
queryParameters: params,
onSendProgress: onSendProgress,
);
} on UnauthorizedError catch (e) {
throw ApiFailure.unauthorized(e.messageError);
} on InternalServerError {
throw const ApiFailure.internalServerError();
} on BadNetworkError {
throw const ApiFailure.connectionError();
} on BadRequestError catch (e) {
throw ApiFailure.badRequest(e.messageError);
} on NotFoundError catch (e) {
throw ApiFailure.notFound(e.messageError);
} on ConnectionTimeoutError {
throw const ApiFailure.connectionTimeout();
} on DioException catch (e) {
var errorMessage =
e.response?.data['message'] ?? e.response?.statusMessage ?? e.error;
if (errorMessage.toString().contains('Connection reset')) {
errorMessage = 'Connection reset';
}
throw ApiFailure.serverError(
statusCode: e.response?.statusCode ?? 0,
errorMessage: errorMessage.toString(),
);
} catch (e, s) {
throw ApiFailure.unexpectedError(errorMessage: e, stackTrace: s);
}
}
Future<Response> delete(
String path, {
dynamic data,
Map<String, dynamic>? headers,
Map<String, dynamic>? params,
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
}) async {
try {
return await _dio.delete(
path,
data: data,
options: Options(
headers: headers,
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
),
queryParameters: params,
);
} on UnauthorizedError catch (e) {
throw ApiFailure.unauthorized(e.messageError);
} on InternalServerError {
throw const ApiFailure.internalServerError();
} on BadNetworkError {
throw const ApiFailure.connectionError();
} on BadRequestError catch (e) {
throw ApiFailure.badRequest(e.messageError);
} on NotFoundError catch (e) {
throw ApiFailure.notFound(e.messageError);
} on DioException catch (e) {
throw ApiFailure.serverError(
statusCode: e.response?.statusCode ?? 0,
errorMessage:
e.response?.data['message'] ?? e.response?.statusMessage ?? e.error,
);
} catch (e, s) {
throw ApiFailure.unexpectedError(errorMessage: e, stackTrace: s);
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_failure.freezed.dart';
@freezed
sealed class ApiFailure with _$ApiFailure {
const ApiFailure._();
const factory ApiFailure.serverError({
required int statusCode,
required Object errorMessage,
}) = _ServerError;
const factory ApiFailure.unexpectedError({
required Object errorMessage,
required StackTrace stackTrace,
}) = _UnexpectedError;
const factory ApiFailure.connectionError() = _ConnectionError;
const factory ApiFailure.internalServerError() = _InternalServerError;
const factory ApiFailure.unauthorized(String? message) = _Unauthorized;
const factory ApiFailure.badRequest(String? message) = _BadRequest;
const factory ApiFailure.notFound(String? message) = _NotFound;
const factory ApiFailure.connectionTimeout() = _ConnectionTimeout;
String toStringFormatted(
BuildContext context, {
String? unauthorizedMessage,
}) {
return switch (this) {
_ServerError(:final statusCode, :final errorMessage) =>
'There is a problem with the server. Status code: $statusCode Error: $errorMessage',
_UnexpectedError() => 'An error occurred. Please try again later.',
_ConnectionError() => 'No Internet',
_InternalServerError() =>
'The server is experiencing problems. Please try again later.',
_Unauthorized(:final message) =>
message ?? unauthorizedMessage ?? 'Session has expired.',
_BadRequest(:final message) =>
message ?? 'There is an incorrect entry. Please check again',
_NotFound(:final message) => message ?? 'Not Found',
_ConnectionTimeout() => 'Connection Timeout',
};
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
class BadNetworkError extends DioException {
final DioException dioError;
BadNetworkError(this.dioError)
: super(
requestOptions: dioError.requestOptions,
error: dioError.error,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
class BadRequestError extends DioException {
final DioException dioError;
final String? messageError;
BadRequestError(this.dioError, this.messageError)
: super(
error: dioError.error,
requestOptions: dioError.requestOptions,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
class ConnectionTimeoutError extends DioException {
final DioException dioError;
ConnectionTimeoutError(this.dioError)
: super(
error: dioError.error,
requestOptions: dioError.requestOptions,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
class InternalServerError extends DioException {
final DioException dioError;
InternalServerError(this.dioError)
: super(
requestOptions: dioError.requestOptions,
error: dioError.error,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
class NotFoundError extends DioException {
final DioException dioError;
final String? messageError;
NotFoundError(this.dioError, this.messageError)
: super(
error: dioError.error,
requestOptions: dioError.requestOptions,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
class UnauthorizedError extends DioException {
final DioException dioError;
final String? messageError;
UnauthorizedError(this.dioError, this.messageError)
: super(
requestOptions: dioError.requestOptions,
error: dioError.error,
response: dioError.response,
type: dioError.type,
message: dioError.message,
stackTrace: dioError.stackTrace,
);
}
@@ -0,0 +1,22 @@
import 'package:dio/dio.dart';
import '../../../injection.dart';
import '../../network/network_client.dart';
import '../errors/bad_network_error.dart';
class BadNetworkErrorInterceptor extends Interceptor {
final _networkClient = getIt<NetworkClient>();
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
final isConnected = await _networkClient.isConnected;
if (err.type == DioExceptionType.connectionTimeout ||
!isConnected ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError) {
return super.onError(BadNetworkError(err), handler);
}
super.onError(err, handler);
}
}
@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import '../errors/bad_request_error.dart';
class BadRequestErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 422 ||
err.response?.statusCode == 400 ||
err.response?.statusCode == 405) {
return super.onError(BadRequestError(err, null), handler);
}
super.onError(err, handler);
}
}
@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import '../errors/connection_timeout_error.dart';
class ConnectionTimeoutErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.type == DioExceptionType.connectionTimeout) {
return super.onError(ConnectionTimeoutError(err), handler);
}
super.onError(err, handler);
}
}
@@ -0,0 +1,51 @@
import 'package:dio/dio.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
class CrashlyticsInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// Log semua error ke Crashlytics
FirebaseCrashlytics.instance.recordError(
err,
err.stackTrace,
reason: _getErrorReason(err),
information: _getErrorInformation(err),
fatal: false,
);
super.onError(err, handler);
}
String _getErrorReason(DioException err) {
final statusCode = err.response?.statusCode;
if (statusCode != null) {
return 'HTTP $statusCode Error: ${err.requestOptions.uri.path}';
}
switch (err.type) {
case DioExceptionType.connectionTimeout:
return 'Connection Timeout';
case DioExceptionType.sendTimeout:
return 'Send Timeout';
case DioExceptionType.receiveTimeout:
return 'Receive Timeout';
case DioExceptionType.connectionError:
return 'Connection Error';
case DioExceptionType.cancel:
return 'Request Cancelled';
default:
return 'Network Error: ${err.type.name}';
}
}
List<String> _getErrorInformation(DioException err) {
return [
'URL: ${err.requestOptions.uri}',
'Method: ${err.requestOptions.method}',
'Status Code: ${err.response?.statusCode ?? 'N/A'}',
'Error Type: ${err.type.name}',
'Message: ${err.message ?? 'No message'}',
];
}
}
@@ -0,0 +1,18 @@
import 'package:dio/dio.dart';
import '../errors/internal_server_error.dart';
class InternalServerErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response != null) {
if (err.response?.statusCode != null &&
err.response!.statusCode! >= 500 &&
err.response!.statusCode! < 600) {
return super.onError(InternalServerError(err), handler);
}
}
super.onError(err, handler);
}
}
@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import '../errors/not_found_error.dart';
class NotFoundErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 404) {
return super.onError(NotFoundError(err, null), handler);
}
super.onError(err, handler);
}
}
@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import '../errors/unauthorized_error.dart';
class UnauthorizedInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 401 ||
err.response?.statusCode == 403 ||
err.response?.statusCode == 419) {
return super.onError(UnauthorizedError(err, null), handler);
}
super.onError(err, handler);
}
}
+1
View File
@@ -0,0 +1 @@
// TODO: define your code
+5
View File
@@ -0,0 +1,5 @@
class AppConstant {
static const String appName = "Apskel POS";
static const int cacheExpire = 10; // in minutes
}
@@ -0,0 +1,5 @@
class LocalStorageKey {
static const String token = 'token';
static const String user = 'user';
static const String outlet = 'outlet';
}
+158
View File
@@ -0,0 +1,158 @@
import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'db_pos.db');
return await openDatabase(
path,
version: 1, // Updated version for categories table
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// Products table
await db.execute('''
CREATE TABLE products (
id TEXT PRIMARY KEY,
organization_id TEXT,
category_id TEXT,
sku TEXT,
name TEXT,
description TEXT,
price INTEGER,
cost INTEGER,
business_type TEXT,
image_url TEXT,
printer_type TEXT,
metadata TEXT,
is_active INTEGER,
created_at TEXT,
updated_at TEXT
)
''');
// Product Variants table
await db.execute('''
CREATE TABLE product_variants (
id TEXT PRIMARY KEY,
product_id TEXT,
name TEXT,
price_modifier INTEGER,
cost INTEGER,
metadata TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
)
''');
// Categories table - NEW
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
// Printer table
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
// Create indexes for better performance
await db.execute(
'CREATE INDEX idx_products_category_id ON products(category_id)',
);
await db.execute('CREATE INDEX idx_products_name ON products(name)');
await db.execute('CREATE INDEX idx_products_sku ON products(sku)');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)',
);
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)',
);
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add printer table in version 2
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
if (oldVersion < 3) {
// Add categories table in version 3
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)',
);
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)',
);
}
}
Future<void> close() async {
final db = await database;
await db.close();
_database = null;
}
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:injectable/injectable.dart';
import '../../presentation/router/app_router.dart';
@module
abstract class AutoRouteDi {
@lazySingleton
AppRouter get appRouter => AppRouter();
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:injectable/injectable.dart';
@module
abstract class ConnectivityDi {
@lazySingleton
Connectivity get connectivity => Connectivity();
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:injectable/injectable.dart';
import '../database/database_helper.dart';
@module
abstract class DatabaseDi {
@singleton
DatabaseHelper get databaseHelper => DatabaseHelper();
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
@module
abstract class DioDi {
@lazySingleton
Dio get dio => Dio();
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
@module
abstract class SharedPreferencesDi {
@preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}
@@ -0,0 +1,7 @@
part of 'extension.dart';
extension BuildContextExt on BuildContext {
double get deviceHeight => MediaQuery.of(this).size.height;
double get deviceWidth => MediaQuery.of(this).size.width;
}
+3
View File
@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';
part 'build_context_extension.dart';
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../injection.dart';
import '../constant/local_storage_key.dart';
void dismissKeyboard(BuildContext context) {
final currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
FocusManager.instance.primaryFocus?.unfocus();
}
}
Map<String, dynamic> getAuthorizationHeader() {
return {
'Authorization':
'Bearer ${getIt<SharedPreferences>().getString(LocalStorageKey.token)}',
};
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:injectable/injectable.dart';
@lazySingleton
class NetworkClient extends NetworkInfoBase {
final Connectivity connectivity;
NetworkClient(this.connectivity);
@override
Future<bool> get isConnected async {
final result = await connectivity.checkConnectivity();
return result.first != ConnectivityResult.none;
}
}
abstract class NetworkInfoBase {
Future<bool> get isConnected;
}
+68
View File
@@ -0,0 +1,68 @@
part of 'theme.dart';
class AppColor {
// Primary Colors
static const Color primary = Color(0xFF36175e);
static const Color primaryLight = Color(0xFF5a2d85);
static const Color primaryDark = Color(0xFF1e0d35);
// Secondary Colors
static const Color secondary = Color(0xFF4CAF50);
static const Color secondaryLight = Color(0xFF81C784);
static const Color secondaryDark = Color(0xFF388E3C);
// Background Colors
static const Color background = Color(0xFFF1F5F9);
static const Color backgroundLight = Color(0xFFFFFFFF);
static const Color backgroundDark = Color(0xFF1A1A1A);
static const Color surface = Color(0xFFFFFFFF);
static const Color surfaceDark = Color(0xFF2D2D2D);
// Text Colors
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
static const Color textLight = Color(0xFFBDBDBD);
static const Color textWhite = Color(0xFFFFFFFF);
// Status Colors
static const Color success = Color(0xFF4CAF50);
static const Color error = Color(0xFFE53E3E);
static const Color warning = Color(0xFFFF9800);
static const Color info = Color(0xFF2196F3);
// Border Colors
static const Color border = Color(0xFFE0E0E0);
static const Color borderLight = Color(0xFFF0F0F0);
static const Color borderDark = Color(0xFFBDBDBD);
// Basic Color
static const Color white = Color(0xFFFFFFFF);
static const Color black = Color(0xFF000000);
static const Color disabled = Color(0xFFC8D1E1);
// Gradient Colors
static const List<Color> primaryGradient = [
Color(0xFF36175e),
Color(0xFF5a2d85),
];
static const List<Color> successGradient = [
Color(0xFF4CAF50),
Color(0xFF81C784),
];
static const List<Color> backgroundGradient = [
Color(0xFFF5F5F5),
Color(0xFFE8E8E8),
];
// Opacity Variations
static Color primaryWithOpacity(double opacity) =>
primary.withOpacity(opacity);
static Color successWithOpacity(double opacity) =>
success.withOpacity(opacity);
static Color errorWithOpacity(double opacity) => error.withOpacity(opacity);
static Color warningWithOpacity(double opacity) =>
warning.withOpacity(opacity);
}
+27
View File
@@ -0,0 +1,27 @@
part of 'theme.dart';
class AppStyle {
static TextStyle xs = TextStyle(color: AppColor.black, fontSize: 11);
static TextStyle sm = TextStyle(color: AppColor.black, fontSize: 12);
static TextStyle md = TextStyle(color: AppColor.black, fontSize: 14);
static TextStyle lg = TextStyle(color: AppColor.black, fontSize: 16);
static TextStyle xl = TextStyle(color: AppColor.black, fontSize: 18);
static TextStyle xxl = TextStyle(color: AppColor.black, fontSize: 20);
static TextStyle h6 = TextStyle(color: AppColor.black, fontSize: 22);
static TextStyle h5 = TextStyle(color: AppColor.black, fontSize: 24);
static TextStyle h4 = TextStyle(color: AppColor.black, fontSize: 26);
static TextStyle h3 = TextStyle(color: AppColor.black, fontSize: 28);
static TextStyle h2 = TextStyle(color: AppColor.black, fontSize: 30);
static TextStyle h1 = TextStyle(color: AppColor.black, fontSize: 32);
}

Some files were not shown because too many files have changed in this diff Show More