first commit

This commit is contained in:
2026-04-29 12:53:22 +07:00
commit e6a30eddd3
394 changed files with 16408 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
typedef NavResult<T> = void Function(T? result);
typedef NavOpened = void Function();
class AppNavigator {
AppNavigator._();
static Future<T?> push<T extends Object?>(
BuildContext context,
String route, {
Object? extra,
NavOpened? onOpened,
NavResult<T>? onResult,
}) async {
final future = context.push<T>(route, extra: extra);
if (onOpened != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onOpened();
});
}
if (onResult != null) {
await future.then(onResult);
}
return future;
}
static Future<void> clearAndPush(
BuildContext context,
String route, {
Object? extra,
NavOpened? onOpened,
}) async {
context.go(route, extra: extra);
if (onOpened != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onOpened();
});
}
}
static void pop<T extends Object?>(
BuildContext context, [
T? result,
]) {
context.pop(result);
}
static void popToRoot(BuildContext context) {
while (context.canPop()) {
context.pop();
}
}
static Future<bool> maybePop(BuildContext context) async {
if (context.canPop()) {
context.pop();
return true;
}
return false;
}
static Future<T?> pushFade<T extends Object?>(
BuildContext context,
String route, {
Object? extra,
Duration duration = const Duration(milliseconds: 200),
NavOpened? onOpened,
NavResult<T>? onResult,
}) async {
final future = context.push<T>(
route,
extra: extra,
);
if (onOpened != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onOpened();
});
}
if (onResult != null) {
await future.then(onResult);
}
return future;
}
static Future<T?> pushSlide<T extends Object?>(
BuildContext context,
String route, {
Object? extra,
Duration duration = const Duration(milliseconds: 250),
NavOpened? onOpened,
NavResult<T>? onResult,
}) async {
final future = context.push<T>(
route,
extra: extra,
);
if (onOpened != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onOpened();
});
}
if (onResult != null) {
await future.then(onResult);
}
return future;
}
}

View File

@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
class _OneShotResumeObserver with WidgetsBindingObserver {
_OneShotResumeObserver(this._onResume);
final Future<void> Function() _onResume;
bool _done = false;
void start() => WidgetsBinding.instance.addObserver(this);
void stop() {
if (_done) return;
_done = true;
WidgetsBinding.instance.removeObserver(this);
}
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.resumed && !_done) {
try {
await _onResume();
} finally {
stop();
}
}
}
}
Future<void> waitRecheckOnNextResume(
Future<void> Function() onResume, {
Duration timeout = const Duration(seconds: 45),
}) async {
final c = Completer<void>();
final obs = _OneShotResumeObserver(() async {
try {
await onResume();
} finally {
if (!c.isCompleted) c.complete();
}
})..start();
Future.delayed(timeout, () {
obs.stop();
if (!c.isCompleted) c.complete();
});
return c.future;
}

View File

@@ -0,0 +1,59 @@
import 'package:frontend_eccp_mobile/app/core/constants/globalkey.dart';
import 'package:frontend_eccp_mobile/app/core/navigation/app_routes.dart';
import 'package:frontend_eccp_mobile/app/core/navigation/main_navigation_page.dart';
import 'package:frontend_eccp_mobile/modules/auth/login/presentation/screen/login_page.dart';
import 'package:frontend_eccp_mobile/modules/splash/presentation/screen/splash_screen.dart';
import 'package:frontend_eccp_mobile/modules/update/presentation/update_page.dart';
import 'package:go_router/go_router.dart';
class AppRouter {
AppRouter._();
static final router = GoRouter(
initialLocation: AppRoutes.splash,
navigatorKey: navigatorKey,
routes: [
GoRoute(
path: AppRoutes.splash,
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: AppRoutes.updateApp,
builder: (context, state) {
final extra = state.extra as Map<String, dynamic>?;
final version = extra?['version'] as String? ?? '';
final link = extra?['link'] as String? ?? '';
return UpdatePage(
version: version,
link: link,
);
},
),
GoRoute(
path: AppRoutes.login,
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: AppRoutes.home,
builder: (context, state) => const MainNavigationPage(),
),
GoRoute(
path: AppRoutes.berkas,
builder: (context, state) => const MainNavigationPage(
initialIndex: 1,
),
),
GoRoute(
path: AppRoutes.settings,
builder: (context, state) => const MainNavigationPage(
initialIndex: 2,
),
),
],
);
}

View File

@@ -0,0 +1,10 @@
class AppRoutes {
AppRoutes._();
static const splash = '/';
static const login = '/login';
static const updateApp = '/update-app';
static const home = '/home';
static const settings = '/settings';
static const berkas = '/berkas';
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:frontend_eccp_mobile/app/core/widgets/app_bottom_nav.dart';
import 'package:frontend_eccp_mobile/modules/berkas/presentation/screen/berkas_page.dart';
import 'package:frontend_eccp_mobile/modules/home/presentation/screen/home_page.dart';
import 'package:frontend_eccp_mobile/modules/profile/presentation/screen/profile_page.dart';
class MainNavigationPage extends StatefulWidget {
const MainNavigationPage({
super.key,
this.initialIndex = 0,
});
final int initialIndex;
@override
State<MainNavigationPage> createState() => _MainNavigationPageState();
}
class _MainNavigationPageState extends State<MainNavigationPage> {
late int _currentIndex;
final List<Widget> _pages = [
const HomePage(),
const BerkasPage(),
const ProfilePage(),
];
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
}
void _onTabChanged(int index) {
if (_currentIndex == index) return;
setState(() => _currentIndex = index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: AppBottomNav(
currentIndex: _currentIndex,
onTap: _onTabChanged,
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'dart:io';
import 'package:android_intent_plus/android_intent.dart';
import 'package:app_settings/app_settings.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart' as ph;
import 'package:url_launcher/url_launcher.dart';
class SettingsNavigator {
SettingsNavigator._();
static Future<bool> openAppSettings() async {
try {
final ok = await Geolocator.openAppSettings();
if (ok) return true;
} catch (_) {}
try {
final ok = await ph.openAppSettings();
if (ok) return true;
} catch (_) {}
try {
await AppSettings.openAppSettings();
return true;
} catch (_) {}
if (Platform.isIOS) {
try {
final uri = Uri.parse('app-settings:');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
return true;
}
} catch (_) {}
}
return false;
}
static Future<bool> openLocationSettings() async {
if (Platform.isIOS) return openAppSettings();
try {
final ok = await Geolocator.openLocationSettings();
if (ok) return true;
} catch (_) {}
try {
const i = AndroidIntent(
action: 'android.settings.LOCATION_SOURCE_SETTINGS',
);
await i.launch();
return true;
} catch (_) {}
return openAppSettings();
}
}