feat: implement spine architecture and main pages

This commit is contained in:
NaiJi 2022-12-27 07:19:49 +04:00
parent 83e963dc55
commit 8e44718def
16 changed files with 816 additions and 26 deletions

View file

@ -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<AppSettingsState> {
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));
}
}

View file

@ -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<Object> get props => [key];
}

118
lib/logic/config.dart Normal file
View file

@ -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<void> getItSetup() async {
getIt.registerSingleton<NavigationService>(NavigationService());
getIt.registerSingleton<TimerModel>(TimerModel());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
await getIt.allReady();
}
class ApiConfigModel {
final Box _box = Hive.box(BNames.serverInstallationBox);
String? get serverKey => _serverKey;
String? _serverKey;
Future<void> 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<void> 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<Uint8List> getEncryptedKey(final String encKey) async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey);
if (!hasEncryptionKey) {
final List<int> 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<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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,
);
}

View file

@ -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<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light, // Manually changing appbar color
child: BlocAndProviderConfig(
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (
final BuildContext context,
final AppSettingsState appSettings,
) =>
MaterialApp(
scaffoldMessengerKey:
getIt.get<NavigationService>().scaffoldMessengerKey,
navigatorKey: getIt.get<NavigationService>().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!;
},
),
),
),
);
}

13
lib/models/courier.dart Normal file
View file

@ -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;
}

13
lib/models/item.dart Normal file
View file

@ -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;
}

30
lib/models/order.dart Normal file
View file

@ -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<Item> items;
}

57
lib/ui/brandtabbar.dart Normal file
View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
class BrandTabBar extends StatefulWidget {
const BrandTabBar({super.key, this.controller});
final TabController? controller;
@override
State<BrandTabBar> createState() => _BrandTabBarState();
}
class _BrandTabBarState extends State<BrandTabBar> {
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,
);
}

44
lib/ui/courier.dart Normal file
View file

@ -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),
),
],
),
),
);
}

88
lib/ui/courierspage.dart Normal file
View file

@ -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<CouriersCubit, CouriersState>(
builder: (final BuildContext context, final CouriersState state) {
final List<Courier> couriers = state.couriers;
if (couriers.isEmpty) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<CouriersCubit>().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<CouriersCubit>().refresh();
},
child: Text(
'Обновить',
style: Theme.of(context).textTheme.button?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
],
),
),
),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<CouriersCubit>().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,
);
}
}

44
lib/ui/item.dart Normal file
View file

@ -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),
),
],
),
),
);
}

87
lib/ui/itemspage.dart Normal file
View file

@ -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<ItemsCubit, ItemsState>(
builder: (final BuildContext context, final ItemsState state) {
final List<Item> users = state.users;
if (users.isEmpty) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<ItemsCubit>().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<ItemsCubit>().refresh();
},
child: Text(
'Обновить',
style: Theme.of(context).textTheme.button?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
],
),
),
),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<ItemsCubit>().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,
);
}
}

44
lib/ui/order.dart Normal file
View file

@ -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),
),
],
),
),
);
}

88
lib/ui/orderspage.dart Normal file
View file

@ -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<OrdersCubit, OrdersState>(
builder: (final BuildContext context, final OrdersState state) {
final List<Order> orders = state.orders;
if (orders.isEmpty) {
if (state.isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<OrdersCubit>().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<OrdersCubit>().refresh();
},
child: Text(
'Обновить',
style: Theme.of(context).textTheme.button?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
),
],
),
),
),
);
}
return RefreshIndicator(
onRefresh: () async {
context.read<OrdersCubit>().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,
);
}
}

67
lib/ui/root.dart Normal file
View file

@ -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<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> 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<ChangeTab>(
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<int> onPress;
}

View file

@ -64,28 +64,3 @@ flutter_icons:
ios: true ios: true
image_path_android: "assets/images/icon/logo_android.png" image_path_android: "assets/images/icon/logo_android.png"
image_path_ios: "assets/images/icon/logo_ios.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