diff --git a/lib/logic/appsettingscubit.dart b/lib/logic/appsettingscubit.dart new file mode 100644 index 0000000..00a0390 --- /dev/null +++ b/lib/logic/appsettingscubit.dart @@ -0,0 +1,35 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:selfprivacy/logic/config.dart'; + +export 'package:provider/provider.dart'; +import 'package:equatable/equatable.dart'; + +part 'appsettingsstate.dart'; + +class AppSettingsCubit extends Cubit { + AppSettingsCubit({ + required final String key, + }) : super( + AppSettingsState( + key: key, + ), + ); + + Box box = Hive.box(BNames.appSettingsBox); + + void load() { + final String? key = box.get(BNames.serverKey); + emit( + state.copyWith( + key: key, + ), + ); + } + + void updateServerKey({required final String key}) { + box.put(BNames.serverKey, key); + emit(state.copyWith(key: key)); + } +} diff --git a/lib/logic/appsettingsstate.dart b/lib/logic/appsettingsstate.dart new file mode 100644 index 0000000..052546a --- /dev/null +++ b/lib/logic/appsettingsstate.dart @@ -0,0 +1,19 @@ +part of 'appsettingscubit.dart'; + +class AppSettingsState extends Equatable { + const AppSettingsState({ + required this.key, + }); + + final String key; + + AppSettingsState copyWith({ + final String? key, + }) => + AppSettingsState( + key: key ?? this.key, + ); + + @override + List get props => [key]; +} diff --git a/lib/logic/config.dart b/lib/logic/config.dart new file mode 100644 index 0000000..7c3ab1c --- /dev/null +++ b/lib/logic/config.dart @@ -0,0 +1,118 @@ +import 'package:get_it/get_it.dart'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; +import 'package:selfprivacy/logic/appsettingscubit.dart'; + +final GetIt getIt = GetIt.instance; + +Future getItSetup() async { + getIt.registerSingleton(NavigationService()); + + getIt.registerSingleton(TimerModel()); + getIt.registerSingleton(ApiConfigModel()..init()); + + await getIt.allReady(); +} + +class ApiConfigModel { + final Box _box = Hive.box(BNames.serverInstallationBox); + + String? get serverKey => _serverKey; + String? _serverKey; + + Future storeServerProviderKey(final String value) async { + await _box.put(BNames.serverKey, value); + _serverKey = value; + } + + void clear() { + _serverKey = null; + } + + void init() { + _serverKey = _box.get(BNames.serverKey); + } +} + +class HiveConfig { + static Future init() async { + await Hive.initFlutter(); + + await Hive.openBox(BNames.appSettingsBox); + + final HiveAesCipher cipher = HiveAesCipher( + await getEncryptedKey(BNames.serverInstallationEncryptionKey), + ); + + await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); + } + + static Future getEncryptedKey(final String encKey) async { + const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey); + if (!hasEncryptionKey) { + final List key = Hive.generateSecureKey(); + await secureStorage.write(key: encKey, value: base64UrlEncode(key)); + } + + final String? string = await secureStorage.read(key: encKey); + return base64Url.decode(string!); + } +} + +class BNames { + static String appSettingsBox = 'appSettingsBox'; + static String serverInstallationEncryptionKey = 'key'; + static String serverInstallationBox = 'appConfig'; + static String rootKeys = 'rootKeys'; + static String serverKey = 'serverKey'; +} + +class NavigationService { + final GlobalKey scaffoldMessengerKey = + GlobalKey(); + final GlobalKey navigatorKey = GlobalKey(); + + NavigatorState? get navigator => navigatorKey.currentState; + + void showPopUpDialog(final AlertDialog dialog) { + final BuildContext context = navigatorKey.currentState!.overlay!.context; + + showDialog( + context: context, + builder: (final _) => dialog, + ); + } +} + +class TimerModel extends ChangeNotifier { + DateTime _time = DateTime.now(); + + DateTime get time => _time; + + void restart() { + _time = DateTime.now(); + notifyListeners(); + } +} + +class BlocAndProviderConfig extends StatelessWidget { + const BlocAndProviderConfig({super.key, this.child}); + + final Widget? child; + + @override + Widget build(final BuildContext context) => MultiProvider( + providers: [ + BlocProvider( + create: (final _) => AppSettingsCubit(key: '')..load(), + ), + ], + child: child, + ); +} diff --git a/lib/main.dart b/lib/main.dart index f76cc54..7dc0bbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1 +1,69 @@ -void main() async {} +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/logic/appsettingscubit.dart'; +import 'package:selfprivacy/logic/config.dart'; +import 'package:selfprivacy/ui/root.dart'; +import 'package:wakelock/wakelock.dart'; +import 'package:timezone/data/latest.dart' as tz; + +class SimpleBlocObserver extends BlocObserver { + SimpleBlocObserver(); +} + +void main() async { + await HiveConfig.init(); + WidgetsFlutterBinding.ensureInitialized(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + try { + await Wakelock.enable(); + } on PlatformException catch (e) { + print(e); + } + + await getItSetup(); + tz.initializeTimeZones(); + + BlocOverrides.runZoned( + () => runApp(const MyApp()), + blocObserver: SimpleBlocObserver(), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({ + super.key, + }); + + @override + Widget build(final BuildContext context) => + AnnotatedRegion( + value: SystemUiOverlayStyle.light, // Manually changing appbar color + child: BlocAndProviderConfig( + child: BlocBuilder( + builder: ( + final BuildContext context, + final AppSettingsState appSettings, + ) => + MaterialApp( + scaffoldMessengerKey: + getIt.get().scaffoldMessengerKey, + navigatorKey: getIt.get().navigatorKey, + debugShowCheckedModeBanner: false, + title: 'Admin Panel', + home: const RootPage(), + builder: (final BuildContext context, final Widget? widget) { + Widget error = const Text('...rendering error...'); + if (widget is Scaffold || widget is Navigator) { + error = Scaffold(body: Center(child: error)); + } + ErrorWidget.builder = + (final FlutterErrorDetails errorDetails) => error; + return widget!; + }, + ), + ), + ), + ); +} diff --git a/lib/models/courier.dart b/lib/models/courier.dart new file mode 100644 index 0000000..92ceead --- /dev/null +++ b/lib/models/courier.dart @@ -0,0 +1,13 @@ +class Courier { + Courier({ + required this.name, + required this.surname, + required this.id, + required this.phone, + }); + + final String name; + final String surname; + final String phone; + final int id; +} diff --git a/lib/models/item.dart b/lib/models/item.dart new file mode 100644 index 0000000..50e47a3 --- /dev/null +++ b/lib/models/item.dart @@ -0,0 +1,13 @@ +class Item { + Item({ + required this.title, + required this.description, + required this.id, + required this.price, + }); + + final String title; + final String description; + final int price; + final int id; +} diff --git a/lib/models/order.dart b/lib/models/order.dart new file mode 100644 index 0000000..5842c81 --- /dev/null +++ b/lib/models/order.dart @@ -0,0 +1,30 @@ +import 'package:selfprivacy/models/item.dart'; + +class Order { + Order({ + required this.title, + required this.description, + required this.id, + required this.customerName, + required this.customerPhone, + required this.orderDate, + required this.deliveryDate, + required this.address, + required this.status, + required this.courierId, + required this.items, + }); + + final String title; + final String description; + final String customerName; + final String customerPhone; + final String orderDate; + final String deliveryDate; + final String address; + final String status; + final int id; + final int courierId; + + final List items; +} diff --git a/lib/ui/brandtabbar.dart b/lib/ui/brandtabbar.dart new file mode 100644 index 0000000..fd31857 --- /dev/null +++ b/lib/ui/brandtabbar.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +class BrandTabBar extends StatefulWidget { + const BrandTabBar({super.key, this.controller}); + + final TabController? controller; + @override + State createState() => _BrandTabBarState(); +} + +class _BrandTabBarState extends State { + int? currentIndex; + @override + void initState() { + currentIndex = widget.controller!.index; + widget.controller!.addListener(_listener); + super.initState(); + } + + void _listener() { + if (currentIndex != widget.controller!.index) { + setState(() { + currentIndex = widget.controller!.index; + }); + } + } + + @override + void dispose() { + widget.controller ?? widget.controller!.removeListener(_listener); + super.dispose(); + } + + @override + Widget build(final BuildContext context) => NavigationBar( + destinations: [ + _getIconButton('Товары', Icons.add_box_outlined, 0), + _getIconButton('Курьеры', Icons.person_pin_outlined, 1), + _getIconButton('Заказы', Icons.confirmation_num_outlined, 2), + ], + onDestinationSelected: (final index) { + widget.controller!.animateTo(index); + }, + selectedIndex: currentIndex ?? 0, + labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, + ); + + NavigationDestination _getIconButton( + final String label, + final IconData iconData, + final int index, + ) => + NavigationDestination( + icon: Icon(iconData), + label: label, + ); +} diff --git a/lib/ui/courier.dart b/lib/ui/courier.dart new file mode 100644 index 0000000..85e8489 --- /dev/null +++ b/lib/ui/courier.dart @@ -0,0 +1,44 @@ +part of 'courierspage.dart'; + +class _Courier extends StatelessWidget { + const _Courier({ + required this.courier, + }); + + final Courier courier; + + @override + Widget build(final BuildContext context) => InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (final BuildContext context) => CourierDetails(courier), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: const BoxDecoration( + color: Color.fromRGBO( + 133, + 133, + 200, + 100, + ), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 20), + Flexible( + child: Text(courier.title), + ), + ], + ), + ), + ); +} diff --git a/lib/ui/courierspage.dart b/lib/ui/courierspage.dart new file mode 100644 index 0000000..2ff64ff --- /dev/null +++ b/lib/ui/courierspage.dart @@ -0,0 +1,88 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/models/courier.dart'; + +part 'courier.dart'; + +class CouriersPage extends StatelessWidget { + const CouriersPage({super.key}); + + @override + Widget build(final BuildContext context) { + final Widget child = BlocBuilder( + builder: (final BuildContext context, final CouriersState state) { + final List couriers = state.couriers; + + if (couriers.isEmpty) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: OutlinedButton( + onPressed: () { + context.read().refresh(); + }, + child: Text( + 'Обновить', + style: Theme.of(context).textTheme.button?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: ListView.builder( + itemCount: couriers.length, + itemBuilder: (final BuildContext context, final int index) => + _Courier( + courier: couriers[index], + ), + ), + ); + }, + ); + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: AppBar( + title: const Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text('Товары'), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + body: child, + ); + } +} diff --git a/lib/ui/item.dart b/lib/ui/item.dart new file mode 100644 index 0000000..e7182fb --- /dev/null +++ b/lib/ui/item.dart @@ -0,0 +1,44 @@ +part of 'itemspage.dart'; + +class _Item extends StatelessWidget { + const _Item({ + required this.item, + }); + + final Item item; + + @override + Widget build(final BuildContext context) => InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (final BuildContext context) => ItemDetails(item), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: const BoxDecoration( + color: Color.fromRGBO( + 133, + 200, + 133, + 100, + ), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 20), + Flexible( + child: Text(item.title), + ), + ], + ), + ), + ); +} diff --git a/lib/ui/itemspage.dart b/lib/ui/itemspage.dart new file mode 100644 index 0000000..99842a0 --- /dev/null +++ b/lib/ui/itemspage.dart @@ -0,0 +1,87 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/models/item.dart'; + +part 'item.dart'; + +class ItemsPage extends StatelessWidget { + const ItemsPage({super.key}); + + @override + Widget build(final BuildContext context) { + final Widget child = BlocBuilder( + builder: (final BuildContext context, final ItemsState state) { + final List users = state.users; + + if (users.isEmpty) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: OutlinedButton( + onPressed: () { + context.read().refresh(); + }, + child: Text( + 'Обновить', + style: Theme.of(context).textTheme.button?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: ListView.builder( + itemCount: users.length, + itemBuilder: (final BuildContext context, final int index) => _Item( + item: users[index], + ), + ), + ); + }, + ); + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: AppBar( + title: const Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text('Товары'), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + body: child, + ); + } +} diff --git a/lib/ui/order.dart b/lib/ui/order.dart new file mode 100644 index 0000000..ad58a03 --- /dev/null +++ b/lib/ui/order.dart @@ -0,0 +1,44 @@ +part of 'orderspage.dart'; + +class _Order extends StatelessWidget { + const _Order({ + required this.order, + }); + + final Order order; + + @override + Widget build(final BuildContext context) => InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (final BuildContext context) => OrderDetails(order), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: const BoxDecoration( + color: Color.fromRGBO( + 133, + 200, + 133, + 100, + ), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 20), + Flexible( + child: Text(order.title), + ), + ], + ), + ), + ); +} diff --git a/lib/ui/orderspage.dart b/lib/ui/orderspage.dart new file mode 100644 index 0000000..a9398ed --- /dev/null +++ b/lib/ui/orderspage.dart @@ -0,0 +1,88 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/models/order.dart'; + +part 'order.dart'; + +class OrdersPage extends StatelessWidget { + const OrdersPage({super.key}); + + @override + Widget build(final BuildContext context) { + final Widget child = BlocBuilder( + builder: (final BuildContext context, final OrdersState state) { + final List orders = state.orders; + + if (orders.isEmpty) { + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: OutlinedButton( + onPressed: () { + context.read().refresh(); + }, + child: Text( + 'Обновить', + style: Theme.of(context).textTheme.button?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: ListView.builder( + itemCount: orders.length, + itemBuilder: (final BuildContext context, final int index) => + _Order( + order: orders[index], + ), + ), + ); + }, + ); + + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: AppBar( + title: const Padding( + padding: EdgeInsets.only(top: 4.0), + child: Text('Товары'), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + body: child, + ); + } +} diff --git a/lib/ui/root.dart b/lib/ui/root.dart new file mode 100644 index 0000000..6402219 --- /dev/null +++ b/lib/ui/root.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/appsettingscubit.dart'; +import 'package:selfprivacy/ui/brandtabbar.dart'; +import 'package:selfprivacy/ui/courierspage.dart'; +import 'package:selfprivacy/ui/itemspage.dart'; +import 'package:selfprivacy/ui/orderspage.dart'; + +class RootPage extends StatefulWidget { + const RootPage({super.key}); + + @override + State createState() => _RootPageState(); +} + +class _RootPageState extends State with TickerProviderStateMixin { + late TabController tabController; + + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + @override + void initState() { + tabController = TabController(length: 3, vsync: this); + tabController.addListener(() { + setState(() { + tabController.index == 2 + ? _controller.forward() + : _controller.reverse(); + }); + }); + super.initState(); + } + + @override + void dispose() { + tabController.dispose(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(final BuildContext context) => SafeArea( + child: Provider( + create: (final _) => ChangeTab(tabController.animateTo), + child: Scaffold( + body: TabBarView( + controller: tabController, + children: const [ + ItemsPage(), + CouriersPage(), + OrdersPage(), + ], + ), + bottomNavigationBar: BrandTabBar( + controller: tabController, + ), + ), + ), + ); +} + +class ChangeTab { + ChangeTab(this.onPress); + final ValueChanged onPress; +} diff --git a/pubspec.yaml b/pubspec.yaml index af1521e..b06f504 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,28 +64,3 @@ flutter_icons: ios: true image_path_android: "assets/images/icon/logo_android.png" image_path_ios: "assets/images/icon/logo_ios.png" - -flutter: - uses-material-design: true - assets: - - assets/images/ - - assets/images/onboarding/ - - assets/images/logos/ - - assets/images/gifs/ - - assets/translations/ - - assets/markdown/ - fonts: - - family: BrandIcons - fonts: - - asset: assets/fonts/BrandIcons.ttf - - family: Inter - fonts: - - asset: assets/fonts/Inter-Regular.ttf - - asset: assets/fonts/Inter-Medium.ttf - weight: 500 - - asset: assets/fonts/Inter-SemiBold.ttf - weight: 600 - - asset: assets/fonts/Inter-Bold.ttf - weight: 700 - - asset: assets/fonts/Inter-ExtraBold.ttf - weight: 800