forked from kherel/selfprivacy.org.app
37 changed files with 1240 additions and 230 deletions
@ -0,0 +1,7 @@ |
|||
targets: |
|||
$default: |
|||
builders: |
|||
json_serializable: |
|||
options: |
|||
create_factory: true |
|||
create_to_json: false |
@ -0,0 +1,24 @@ |
|||
import 'package:bloc/bloc.dart'; |
|||
import 'package:equatable/equatable.dart'; |
|||
import 'package:selfprivacy/config/get_it_config.dart'; |
|||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; |
|||
import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; |
|||
|
|||
part 'server_detailed_info_state.dart'; |
|||
|
|||
class ServerDetailsCubit extends Cubit<ServerDetailsState> { |
|||
ServerDetailsCubit() : super(ServerDetailsInitial()); |
|||
|
|||
ServerDetailsRepository repository = ServerDetailsRepository(); |
|||
|
|||
void check() async { |
|||
var isReadyToCheck = getIt<ApiConfigModel>().hetznerServer != null; |
|||
if (isReadyToCheck) { |
|||
emit(ServerDetailsLoading()); |
|||
var data = await repository.load(); |
|||
emit(Loaded(serverInfo: data, checkTime: DateTime.now())); |
|||
} else { |
|||
emit(ServerDetailsNotReady()); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
import 'package:selfprivacy/logic/api_maps/hetzner.dart'; |
|||
import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; |
|||
|
|||
class ServerDetailsRepository { |
|||
Future<HetznerServerInfo> load() async { |
|||
var client = HetznerApi(); |
|||
return await client.getInfo(); |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
part of 'server_detailed_info_cubit.dart'; |
|||
|
|||
abstract class ServerDetailsState extends Equatable { |
|||
const ServerDetailsState(); |
|||
|
|||
@override |
|||
List<Object> get props => []; |
|||
} |
|||
|
|||
class ServerDetailsInitial extends ServerDetailsState {} |
|||
|
|||
class ServerDetailsLoading extends ServerDetailsState {} |
|||
|
|||
class ServerDetailsNotReady extends ServerDetailsState {} |
|||
|
|||
class Loading extends ServerDetailsState {} |
|||
|
|||
class Loaded extends ServerDetailsState { |
|||
final HetznerServerInfo serverInfo; |
|||
final DateTime checkTime; |
|||
|
|||
Loaded({ |
|||
required this.serverInfo, |
|||
required this.checkTime, |
|||
}); |
|||
|
|||
@override |
|||
List<Object> get props => [serverInfo, checkTime]; |
|||
} |
@ -0,0 +1,89 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'hetzner_server_info.g.dart'; |
|||
|
|||
@JsonSerializable() |
|||
class HetznerServerInfo { |
|||
final int id; |
|||
final String name; |
|||
final ServerStatus status; |
|||
final DateTime created; |
|||
|
|||
@JsonKey(name: 'server_type') |
|||
final HetznerServerTypeInfo serverType; |
|||
|
|||
@JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson) |
|||
final HetznerLocation location; |
|||
|
|||
static HetznerLocation locationFromJson(Map json) => |
|||
HetznerLocation.fromJson(json['location']); |
|||
|
|||
static HetznerServerInfo fromJson(Map<String, dynamic> json) => |
|||
_$HetznerServerInfoFromJson(json); |
|||
|
|||
HetznerServerInfo( |
|||
this.id, |
|||
this.name, |
|||
this.status, |
|||
this.created, |
|||
this.serverType, |
|||
this.location, |
|||
); |
|||
} |
|||
|
|||
enum ServerStatus { |
|||
running, |
|||
initializing, |
|||
starting, |
|||
stopping, |
|||
off, |
|||
deleting, |
|||
migrating, |
|||
rebuilding, |
|||
unknown, |
|||
} |
|||
|
|||
@JsonSerializable() |
|||
class HetznerServerTypeInfo { |
|||
final int cores; |
|||
final num memory; |
|||
final int disk; |
|||
|
|||
final List<HetznerPriceInfo> prices; |
|||
|
|||
HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); |
|||
|
|||
static HetznerServerTypeInfo fromJson(Map<String, dynamic> json) => |
|||
_$HetznerServerTypeInfoFromJson(json); |
|||
} |
|||
|
|||
@JsonSerializable() |
|||
class HetznerPriceInfo { |
|||
HetznerPriceInfo(this.hourly, this.monthly); |
|||
|
|||
@JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice) |
|||
final double hourly; |
|||
|
|||
@JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) |
|||
final double monthly; |
|||
|
|||
static HetznerPriceInfo fromJson(Map<String, dynamic> json) => |
|||
_$HetznerPriceInfoFromJson(json); |
|||
|
|||
static double getPrice(Map json) => double.parse(json['gross'] as String); |
|||
} |
|||
|
|||
@JsonSerializable() |
|||
class HetznerLocation { |
|||
final String country; |
|||
final String city; |
|||
final String description; |
|||
|
|||
@JsonKey(name: 'network_zone') |
|||
final String zone; |
|||
|
|||
HetznerLocation(this.country, this.city, this.description, this.zone); |
|||
|
|||
static HetznerLocation fromJson(Map<String, dynamic> json) => |
|||
_$HetznerLocationFromJson(json); |
|||
} |
@ -0,0 +1,84 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'hetzner_server_info.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
HetznerServerInfo _$HetznerServerInfoFromJson(Map<String, dynamic> json) { |
|||
return HetznerServerInfo( |
|||
json['id'] as int, |
|||
json['name'] as String, |
|||
_$enumDecode(_$ServerStatusEnumMap, json['status']), |
|||
DateTime.parse(json['created'] as String), |
|||
HetznerServerTypeInfo.fromJson(json['server_type'] as Map<String, dynamic>), |
|||
HetznerServerInfo.locationFromJson(json['datacenter'] as Map), |
|||
); |
|||
} |
|||
|
|||
K _$enumDecode<K, V>( |
|||
Map<K, V> enumValues, |
|||
Object? source, { |
|||
K? unknownValue, |
|||
}) { |
|||
if (source == null) { |
|||
throw ArgumentError( |
|||
'A value must be provided. Supported values: ' |
|||
'${enumValues.values.join(', ')}', |
|||
); |
|||
} |
|||
|
|||
return enumValues.entries.singleWhere( |
|||
(e) => e.value == source, |
|||
orElse: () { |
|||
if (unknownValue == null) { |
|||
throw ArgumentError( |
|||
'`$source` is not one of the supported values: ' |
|||
'${enumValues.values.join(', ')}', |
|||
); |
|||
} |
|||
return MapEntry(unknownValue, enumValues.values.first); |
|||
}, |
|||
).key; |
|||
} |
|||
|
|||
const _$ServerStatusEnumMap = { |
|||
ServerStatus.running: 'running', |
|||
ServerStatus.initializing: 'initializing', |
|||
ServerStatus.starting: 'starting', |
|||
ServerStatus.stopping: 'stopping', |
|||
ServerStatus.off: 'off', |
|||
ServerStatus.deleting: 'deleting', |
|||
ServerStatus.migrating: 'migrating', |
|||
ServerStatus.rebuilding: 'rebuilding', |
|||
ServerStatus.unknown: 'unknown', |
|||
}; |
|||
|
|||
HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( |
|||
Map<String, dynamic> json) { |
|||
return HetznerServerTypeInfo( |
|||
json['cores'] as int, |
|||
json['memory'] as num, |
|||
json['disk'] as int, |
|||
(json['prices'] as List<dynamic>) |
|||
.map((e) => HetznerPriceInfo.fromJson(e as Map<String, dynamic>)) |
|||
.toList(), |
|||
); |
|||
} |
|||
|
|||
HetznerPriceInfo _$HetznerPriceInfoFromJson(Map<String, dynamic> json) { |
|||
return HetznerPriceInfo( |
|||
HetznerPriceInfo.getPrice(json['price_hourly'] as Map), |
|||
HetznerPriceInfo.getPrice(json['price_monthly'] as Map), |
|||
); |
|||
} |
|||
|
|||
HetznerLocation _$HetznerLocationFromJson(Map<String, dynamic> json) { |
|||
return HetznerLocation( |
|||
json['country'] as String, |
|||
json['city'] as String, |
|||
json['description'] as String, |
|||
json['network_zone'] as String, |
|||
); |
|||
} |
@ -1,23 +0,0 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
@JsonSerializable(createFactory: false) |
|||
class ServerInfo { |
|||
final String id; |
|||
final String name; |
|||
final ServerStatus status; |
|||
final DateTime created; |
|||
|
|||
ServerInfo(this.id, this.name, this.status, this.created); |
|||
} |
|||
|
|||
enum ServerStatus { |
|||
running, |
|||
initializing, |
|||
starting, |
|||
stopping, |
|||
off, |
|||
deleting, |
|||
migrating, |
|||
rebuilding, |
|||
unknown, |
|||
} |
@ -0,0 +1,51 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; |
|||
import 'package:easy_localization/easy_localization.dart'; |
|||
import 'package:selfprivacy/ui/components/pre_styled_buttons.dart'; |
|||
|
|||
class OnePage extends StatelessWidget { |
|||
const OnePage({ |
|||
Key? key, |
|||
required this.title, |
|||
required this.child, |
|||
}) : super(key: key); |
|||
|
|||
final String title; |
|||
final Widget child; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return SafeArea( |
|||
child: Scaffold( |
|||
appBar: PreferredSize( |
|||
child: Column( |
|||
children: [ |
|||
Container( |
|||
height: 51, |
|||
alignment: Alignment.center, |
|||
padding: EdgeInsets.symmetric(horizontal: 15), |
|||
child: BrandText.h4('basis.details'.tr()), |
|||
), |
|||
BrandDivider(), |
|||
], |
|||
), |
|||
preferredSize: Size.fromHeight(52), |
|||
), |
|||
body: child, |
|||
bottomNavigationBar: SafeArea( |
|||
child: Container( |
|||
decoration: BoxDecoration(boxShadow: kElevationToShadow[3]), |
|||
height: kBottomNavigationBarHeight, |
|||
child: Container( |
|||
color: Theme.of(context).scaffoldBackgroundColor, |
|||
alignment: Alignment.center, |
|||
child: PreStyledButtons.close( |
|||
onPress: () => Navigator.of(context).pop()), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; |
|||
import 'package:easy_localization/easy_localization.dart'; |
|||
|
|||
class PreStyledButtons { |
|||
static Widget close({ |
|||
required VoidCallback onPress, |
|||
}) => |
|||
_CloseButton(onPress: onPress); |
|||
} |
|||
|
|||
class _CloseButton extends StatelessWidget { |
|||
const _CloseButton({Key? key, required this.onPress}) : super(key: key); |
|||
|
|||
final VoidCallback onPress; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return OutlinedButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
BrandText.h4('basis.close'.tr()), |
|||
Icon(Icons.close), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,290 @@ |
|||
import 'package:cubit_form/cubit_form.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:selfprivacy/config/brand_colors.dart'; |
|||
import 'package:selfprivacy/config/brand_theme.dart'; |
|||
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; |
|||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; |
|||
import 'package:selfprivacy/logic/models/state_types.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; |
|||
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; |
|||
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; |
|||
import 'package:easy_localization/easy_localization.dart'; |
|||
import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart'; |
|||
import 'package:selfprivacy/utils/named_font_weight.dart'; |
|||
|
|||
part 'server_settings.dart'; |
|||
|
|||
var navigatorKey = GlobalKey<NavigatorState>(); |
|||
|
|||
class ServerDetails extends StatefulWidget { |
|||
const ServerDetails({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
_ServerDetailsState createState() => _ServerDetailsState(); |
|||
} |
|||
|
|||
class _ServerDetailsState extends State<ServerDetails> |
|||
with SingleTickerProviderStateMixin { |
|||
late TabController tabController; |
|||
|
|||
@override |
|||
void dispose() { |
|||
tabController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
void initState() { |
|||
tabController = TabController(length: 2, vsync: this); |
|||
tabController.addListener(() { |
|||
setState(() {}); |
|||
}); |
|||
super.initState(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
var isReady = context.watch<AppConfigCubit>().state.isFullyInitilized; |
|||
var providerState = isReady ? StateType.stable : StateType.uninitialized; |
|||
|
|||
late String title = 'providers.server.card_title'.tr(); |
|||
|
|||
return TabBarView( |
|||
physics: NeverScrollableScrollPhysics(), |
|||
controller: tabController, |
|||
children: [ |
|||
BlocProvider( |
|||
create: (context) => ServerDetailsCubit()..check(), |
|||
child: Builder(builder: (context) { |
|||
var details = context.watch<ServerDetailsCubit>().state; |
|||
if (details is ServerDetailsLoading || |
|||
details is ServerDetailsInitial) { |
|||
return _TempMessage(message: 'basis.loading'.tr()); |
|||
} else if (details is ServerDetailsNotReady) { |
|||
return _TempMessage(message: 'basis.no_data'.tr()); |
|||
} else if (details is Loaded) { |
|||
var data = details.serverInfo; |
|||
var checkTime = details.checkTime; |
|||
|
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Padding( |
|||
padding: brandPagePadding2, |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
IconStatusMask( |
|||
status: providerState, |
|||
child: Icon( |
|||
BrandIcons.server, |
|||
size: 40, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
SizedBox(width: 10), |
|||
BrandText.h2(title), |
|||
Spacer(), |
|||
Padding( |
|||
padding: EdgeInsets.symmetric( |
|||
vertical: 4, |
|||
horizontal: 2, |
|||
), |
|||
child: PopupMenuButton<_PopupMenuItemType>( |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(10.0), |
|||
), |
|||
onSelected: (_PopupMenuItemType result) { |
|||
switch (result) { |
|||
case _PopupMenuItemType.setting: |
|||
tabController.animateTo(1); |
|||
break; |
|||
} |
|||
}, |
|||
icon: Icon(Icons.more_vert), |
|||
itemBuilder: (BuildContext context) => [ |
|||
PopupMenuItem<_PopupMenuItemType>( |
|||
value: _PopupMenuItemType.setting, |
|||
child: Container( |
|||
padding: EdgeInsets.only(left: 5), |
|||
child: Text('basis.settings'.tr()), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 10), |
|||
BrandText.body1('providers.server.bottom_sheet.1'.tr()), |
|||
SizedBox(height: 30), |
|||
Center(child: BrandText.h2('General information')), |
|||
SizedBox(height: 10), |
|||
Table( |
|||
columnWidths: { |
|||
0: FractionColumnWidth(.5), |
|||
1: FractionColumnWidth(.5), |
|||
}, |
|||
defaultVerticalAlignment: |
|||
TableCellVerticalAlignment.middle, |
|||
children: [ |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Last check'), |
|||
getRowValue(formater.format(checkTime)), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Server Id'), |
|||
getRowValue(data.id.toString()), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Status:'), |
|||
getRowValue( |
|||
'${data.status.toString().split('.')[1].toUpperCase()}', |
|||
isBold: true, |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('CPU'), |
|||
getRowValue( |
|||
data.serverType.cores.toString(), |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Memory'), |
|||
getRowValue( |
|||
'${data.serverType.memory.toString()} GB', |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Disk Local'), |
|||
getRowValue( |
|||
'${data.serverType.disk.toString()} GB', |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Price monthly:'), |
|||
getRowValue( |
|||
'${data.serverType.prices[1].monthly.toString()}', |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Price hourly:'), |
|||
getRowValue( |
|||
'${data.serverType.prices[1].hourly.toString()}', |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 30), |
|||
Center(child: BrandText.h2('Location')), |
|||
SizedBox(height: 10), |
|||
Table( |
|||
columnWidths: { |
|||
0: FractionColumnWidth(.5), |
|||
1: FractionColumnWidth(.5), |
|||
}, |
|||
defaultVerticalAlignment: |
|||
TableCellVerticalAlignment.middle, |
|||
children: [ |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Country'), |
|||
getRowValue( |
|||
'${data.location.country}', |
|||
), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('City'), |
|||
getRowValue(data.location.city), |
|||
], |
|||
), |
|||
TableRow( |
|||
children: [ |
|||
getRowTitle('Description'), |
|||
getRowValue(data.location.description), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
// BrandText.body1('providers.server.bottom_sheet.2'.tr()), |
|||
// SizedBox(height: 10), |
|||
// BrandText.body1('providers.server.bottom_sheet.3'.tr()), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} else { |
|||
throw Exception('wrong state'); |
|||
} |
|||
}), |
|||
), |
|||
_ServerSettings(tabController: tabController), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget getRowTitle(String title) { |
|||
return Padding( |
|||
padding: const EdgeInsets.only(right: 10), |
|||
child: BrandText.h5( |
|||
title, |
|||
textAlign: TextAlign.right, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget getRowValue(String title, {bool isBold = false}) { |
|||
return BrandText.body1( |
|||
title, |
|||
style: isBold |
|||
? TextStyle( |
|||
fontWeight: NamedFontWeight.demiBold, |
|||
) |
|||
: null, |
|||
); |
|||
} |
|||
} |
|||
|
|||
enum _PopupMenuItemType { setting } |
|||
|
|||
class _TempMessage extends StatelessWidget { |
|||
const _TempMessage({ |
|||
Key? key, |
|||
required this.message, |
|||
}) : super(key: key); |
|||
|
|||
final String message; |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return SizedBox( |
|||
height: MediaQuery.of(context).size.height - 100, |
|||
child: Center( |
|||
child: BrandText.body2(message), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
final DateFormat formater = DateFormat('HH:mm:ss'); |
@ -0,0 +1,136 @@ |
|||
part of 'server_details.dart'; |
|||
|
|||
class _ServerSettings extends StatelessWidget { |
|||
const _ServerSettings({ |
|||
Key? key, |
|||
required this.tabController, |
|||
}) : super(key: key); |
|||
|
|||
final TabController tabController; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return ListView( |
|||
padding: brandPagePadding2, |
|||
children: [ |
|||
SizedBox(height: 10), |
|||
Container( |
|||
height: 52, |
|||
alignment: Alignment.centerLeft, |
|||
padding: EdgeInsets.only(left: 1), |
|||
child: Container( |
|||
child: Row( |
|||
children: [ |
|||
IconButton( |
|||
icon: Icon(BrandIcons.arrow_left), |
|||
onPressed: () => tabController.animateTo(0), |
|||
), |
|||
SizedBox(width: 10), |
|||
BrandText.h4('basis.settings'.tr()), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
BrandDivider(), |
|||
SwitcherBlock( |
|||
onChange: (_) {}, |
|||
child: _TextColumn( |
|||
title: 'Allow Auto-upgrade', |
|||
value: 'Wether to allow automatic packages upgrades', |
|||
), |
|||
isActive: true, |
|||
), |
|||
SwitcherBlock( |
|||
onChange: (_) {}, |
|||
child: _TextColumn( |
|||
title: 'Reboot after upgrade', |
|||
value: 'Reboot without prompt after applying updates', |
|||
), |
|||
isActive: false, |
|||
), |
|||
_Button( |
|||
onTap: () {}, |
|||
child: _TextColumn( |
|||
title: 'Server Timezone', |
|||
value: 'Europe/Kyiv', |
|||
), |
|||
), |
|||
_Button( |
|||
onTap: () {}, |
|||
child: _TextColumn( |
|||
title: 'Server Locale', |
|||
value: 'Default', |
|||
), |
|||
), |
|||
_Button( |
|||
onTap: () {}, |
|||
child: _TextColumn( |
|||
hasWarning: true, |
|||
title: 'Factory Reset', |
|||
value: 'Restore default settings on your server', |
|||
), |
|||
) |
|||
], |
|||
); |
|||
} |
|||
} |
|||
|
|||
class _Button extends StatelessWidget { |
|||
const _Button({ |
|||
Key? key, |
|||
required this.onTap, |
|||
required this.child, |
|||
}) : super(key: key); |
|||
|
|||
final Widget child; |
|||
final VoidCallback onTap; |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return InkWell( |
|||
onTap: onTap, |
|||
child: Container( |
|||
padding: EdgeInsets.only(top: 20, bottom: 5), |
|||
decoration: BoxDecoration( |
|||
border: Border( |
|||
bottom: BorderSide(width: 1, color: BrandColors.dividerColor), |
|||
)), |
|||
child: child, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
class _TextColumn extends StatelessWidget { |
|||
const _TextColumn({ |
|||
Key? key, |
|||
required this.title, |
|||
required this.value, |
|||
this.hasWarning = false, |
|||
}) : super(key: key); |
|||
|
|||
final String title; |
|||
final String value; |
|||
final bool hasWarning; |
|||
@ |