loading spaces
This commit is contained in:
parent
dca9ee1939
commit
65028a1231
14 changed files with 629 additions and 86 deletions
|
|
@ -1,33 +1,31 @@
|
|||
// import "dart:io";
|
||||
import "dart:io";
|
||||
|
||||
// import "package:matrix/matrix.dart";
|
||||
// import "package:nexusbot/controllers/settings_controller.dart";
|
||||
// import "package:riverpod/riverpod.dart";
|
||||
// import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
// class ClientController extends AsyncNotifier<Client> {
|
||||
// @override
|
||||
// Future<Client> build() async {
|
||||
// final settings = ref.watch(SettingsController.provider)!;
|
||||
// final client = Client(
|
||||
// "nexusbot",
|
||||
// database: await MatrixSdkDatabase.init(
|
||||
// "NexusBot",
|
||||
// database: await databaseFactoryFfi.openDatabase(inMemoryDatabasePath),
|
||||
// ),
|
||||
// );
|
||||
class ClientController extends AsyncNotifier<Client> {
|
||||
@override
|
||||
Future<Client> build() async {
|
||||
final client = Client(
|
||||
"nexus",
|
||||
database: await MatrixSdkDatabase.init(
|
||||
"nexus",
|
||||
database: await databaseFactoryFfi.openDatabase("./test.db"),
|
||||
),
|
||||
);
|
||||
//mxc
|
||||
await client.checkHomeserver(Uri.https("federated.nexus"));
|
||||
await client.login(
|
||||
LoginType.mLoginPassword,
|
||||
identifier: AuthenticationUserIdentifier(user: "quadradical"),
|
||||
password: File("./password.txt").readAsStringSync(),
|
||||
);
|
||||
|
||||
// await client.checkHomeserver(settings.homeserver);
|
||||
// await client.login(
|
||||
// LoginType.mLoginPassword,
|
||||
// identifier: AuthenticationUserIdentifier(user: settings.name),
|
||||
// password: (await File(settings.botPasswordFile).readAsString()).trim(),
|
||||
// );
|
||||
return client;
|
||||
}
|
||||
|
||||
// return client;
|
||||
// }
|
||||
|
||||
// static final provider = AsyncNotifierProvider<ClientController, Client>(
|
||||
// ClientController.new,
|
||||
// );
|
||||
// }
|
||||
static final provider = AsyncNotifierProvider<ClientController, Client>(
|
||||
ClientController.new,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
36
lib/controllers/spaces_controller.dart
Normal file
36
lib/controllers/spaces_controller.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client_controller.dart";
|
||||
import "package:nexus/models/space.dart";
|
||||
|
||||
class SpacesController extends AsyncNotifier<List<Space>> {
|
||||
@override
|
||||
Future<List<Space>> build() async {
|
||||
final client = await ref.watch(ClientController.provider.future);
|
||||
|
||||
return Future.wait(
|
||||
client.rooms.where((room) => room.isSpace).map((data) async {
|
||||
final thumb = await data.avatar?.getThumbnailUri(
|
||||
client,
|
||||
width: 40,
|
||||
height: 40,
|
||||
);
|
||||
return Space(
|
||||
roomData: data,
|
||||
avatar: thumb == null
|
||||
? null
|
||||
: Image.network(
|
||||
thumb.toString(),
|
||||
width: 40,
|
||||
headers: {"authorization": "Bearer ${client.accessToken}"},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider<SpacesController, List<Space>>(
|
||||
SpacesController.new,
|
||||
);
|
||||
}
|
||||
17
lib/helpers/extension_helper.dart
Normal file
17
lib/helpers/extension_helper.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/widgets/error_dialog.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
|
||||
extension BetterWhen<T> on AsyncValue<T> {
|
||||
Widget betterWhen({
|
||||
required Widget Function(T value) data,
|
||||
Widget Function() loading = Loading.new,
|
||||
bool skipLoadingOnRefresh = false,
|
||||
}) => when(
|
||||
data: data,
|
||||
error: (error, stackTrace) => ErrorDialog(error, stackTrace),
|
||||
loading: loading,
|
||||
skipLoadingOnRefresh: skipLoadingOnRefresh,
|
||||
);
|
||||
}
|
||||
10
lib/models/space.dart
Normal file
10
lib/models/space.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import "package:flutter/widgets.dart";
|
||||
import "package:freezed_annotation/freezed_annotation.dart";
|
||||
import "package:matrix/matrix.dart";
|
||||
part "space.freezed.dart";
|
||||
|
||||
@freezed
|
||||
abstract class Space with _$Space {
|
||||
const factory Space({required Room roomData, required Image? avatar}) =
|
||||
_Space;
|
||||
}
|
||||
274
lib/models/space.freezed.dart
Normal file
274
lib/models/space.freezed.dart
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// 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 'space.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$Space {
|
||||
|
||||
Room get roomData; Image? get avatar;
|
||||
/// Create a copy of Space
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SpaceCopyWith<Space> get copyWith => _$SpaceCopyWithImpl<Space>(this as Space, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Space&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.avatar, avatar) || other.avatar == avatar));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,roomData,avatar);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Space(roomData: $roomData, avatar: $avatar)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SpaceCopyWith<$Res> {
|
||||
factory $SpaceCopyWith(Space value, $Res Function(Space) _then) = _$SpaceCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
Room roomData, Image? avatar
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SpaceCopyWithImpl<$Res>
|
||||
implements $SpaceCopyWith<$Res> {
|
||||
_$SpaceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Space _self;
|
||||
final $Res Function(Space) _then;
|
||||
|
||||
/// Create a copy of Space
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? roomData = null,Object? avatar = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable
|
||||
as Room,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||
as Image?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [Space].
|
||||
extension SpacePatterns on Space {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Space value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Space() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Space value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Space():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Space value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Space() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Room roomData, Image? avatar)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Space() when $default != null:
|
||||
return $default(_that.roomData,_that.avatar);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Room roomData, Image? avatar) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Space():
|
||||
return $default(_that.roomData,_that.avatar);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Room roomData, Image? avatar)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Space() when $default != null:
|
||||
return $default(_that.roomData,_that.avatar);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _Space implements Space {
|
||||
const _Space({required this.roomData, required this.avatar});
|
||||
|
||||
|
||||
@override final Room roomData;
|
||||
@override final Image? avatar;
|
||||
|
||||
/// Create a copy of Space
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SpaceCopyWith<_Space> get copyWith => __$SpaceCopyWithImpl<_Space>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Space&&(identical(other.roomData, roomData) || other.roomData == roomData)&&(identical(other.avatar, avatar) || other.avatar == avatar));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,roomData,avatar);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Space(roomData: $roomData, avatar: $avatar)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SpaceCopyWith<$Res> implements $SpaceCopyWith<$Res> {
|
||||
factory _$SpaceCopyWith(_Space value, $Res Function(_Space) _then) = __$SpaceCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
Room roomData, Image? avatar
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SpaceCopyWithImpl<$Res>
|
||||
implements _$SpaceCopyWith<$Res> {
|
||||
__$SpaceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Space _self;
|
||||
final $Res Function(_Space) _then;
|
||||
|
||||
/// Create a copy of Space
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? roomData = null,Object? avatar = freezed,}) {
|
||||
return _then(_Space(
|
||||
roomData: null == roomData ? _self.roomData : roomData // ignore: cast_nullable_to_non_nullable
|
||||
as Room,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
|
||||
as Image?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
32
lib/widgets/error_dialog.dart
Normal file
32
lib/widgets/error_dialog.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:flutter_riverpod/misc.dart";
|
||||
|
||||
class ErrorDialog extends ConsumerWidget {
|
||||
final Object error;
|
||||
final StackTrace? stackTrace;
|
||||
final ProviderOrFamily? provider;
|
||||
const ErrorDialog(this.error, this.stackTrace, {this.provider, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AlertDialog(
|
||||
title: Text("An Error Occurred"),
|
||||
content: SingleChildScrollView(
|
||||
child: SelectableText("$error\n\n$stackTrace"),
|
||||
),
|
||||
actions: [
|
||||
if (provider != null)
|
||||
TextButton(
|
||||
onPressed: () => ref.invalidate(provider!),
|
||||
child: const Text("Try Again"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).popUntil((route) => route.isFirst),
|
||||
child: const Text("Go Back"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
13
lib/widgets/loading.dart
Normal file
13
lib/widgets/loading.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
class Loading extends StatelessWidget {
|
||||
const Loading({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ class RoomChat extends HookConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final urlRegex = RegExp(r"https?://[^\s\]\(\)]+");
|
||||
final controller = RoomChatController.provider("1");
|
||||
final theme = Theme.of(context);
|
||||
return Chat(
|
||||
|
|
@ -25,7 +26,10 @@ class RoomChat extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
builders: Builders(
|
||||
composerBuilder: (_) => Composer(),
|
||||
composerBuilder: (_) => Composer(
|
||||
sendIconColor: theme.colorScheme.primary,
|
||||
sendOnEnter: true,
|
||||
),
|
||||
textMessageBuilder:
|
||||
(
|
||||
context,
|
||||
|
|
@ -36,9 +40,7 @@ class RoomChat extends HookConsumerWidget {
|
|||
}) => FlyerChatTextMessage(
|
||||
message: message.copyWith(
|
||||
text: message.text.replaceAllMapped(
|
||||
RegExp(
|
||||
r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
|
||||
),
|
||||
urlRegex,
|
||||
(match) => "[${match.group(0)}](${match.group(0)})",
|
||||
),
|
||||
),
|
||||
|
|
@ -49,26 +51,22 @@ class RoomChat extends HookConsumerWidget {
|
|||
sentLinksColor: Colors.blue,
|
||||
receivedLinksColor: Colors.blue,
|
||||
),
|
||||
linkPreviewBuilder: (_, message, isSentByMe) {
|
||||
return LinkPreview(
|
||||
text: message.text,
|
||||
backgroundColor: isSentByMe
|
||||
? theme.colorScheme.inversePrimary
|
||||
: theme.colorScheme.surfaceContainerLow,
|
||||
insidePadding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
linkPreviewData: message.linkPreviewData,
|
||||
onLinkPreviewDataFetched: (linkPreviewData) {
|
||||
ref
|
||||
.watch(controller)
|
||||
.updateMessage(
|
||||
message,
|
||||
message.copyWith(linkPreviewData: linkPreviewData),
|
||||
);
|
||||
},
|
||||
// You can still customize the appearance
|
||||
parentContent: message.text,
|
||||
);
|
||||
},
|
||||
linkPreviewBuilder: (_, message, isSentByMe) => LinkPreview(
|
||||
text: urlRegex.firstMatch(message.text)?.group(0) ?? "",
|
||||
backgroundColor: isSentByMe
|
||||
? theme.colorScheme.inversePrimary
|
||||
: theme.colorScheme.surfaceContainerLow,
|
||||
insidePadding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
linkPreviewData: message.linkPreviewData,
|
||||
onLinkPreviewDataFetched: (linkPreviewData) {
|
||||
ref
|
||||
.watch(controller)
|
||||
.updateMessage(
|
||||
message,
|
||||
message.copyWith(linkPreviewData: linkPreviewData),
|
||||
);
|
||||
},
|
||||
),
|
||||
imageMessageBuilder:
|
||||
(
|
||||
_,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:color_hash/color_hash.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/spaces_controller.dart";
|
||||
|
||||
class Sidebar extends HookWidget {
|
||||
class Sidebar extends HookConsumerWidget {
|
||||
const Sidebar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final index = useState(0);
|
||||
return Drawer(
|
||||
shape: Border(),
|
||||
child: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
scrollable: true,
|
||||
useIndicator: false,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
onDestinationSelected: (value) => index.value = value,
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
|
|
@ -28,11 +29,39 @@ class Sidebar extends HookWidget {
|
|||
label: Text("Messages"),
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Image.file(File("assets/icon.png"), width: 40),
|
||||
label: Text("Space 1"),
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
),
|
||||
...ref
|
||||
.watch(SpacesController.provider)
|
||||
.when(
|
||||
loading: () => [],
|
||||
error: (error, stack) {
|
||||
debugPrintStack(
|
||||
label: error.toString(),
|
||||
stackTrace: stack,
|
||||
);
|
||||
throw error;
|
||||
},
|
||||
data: (spaces) => spaces.map(
|
||||
(space) => NavigationRailDestination(
|
||||
icon: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child:
|
||||
space.avatar ??
|
||||
ColoredBox(
|
||||
color: ColorHash(space.roomData.name).color,
|
||||
child: Center(
|
||||
child: Text(space.roomData.name[0]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Text(space.roomData.name),
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
selectedIndex: index.value,
|
||||
),
|
||||
|
|
@ -44,6 +73,7 @@ class Sidebar extends HookWidget {
|
|||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
body: NavigationRail(
|
||||
scrollable: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
extended: true,
|
||||
destinations: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue