Leave room support, persist last room, fixes

This commit is contained in:
Henry Hiles 2025-12-03 16:16:30 -05:00
commit 7dfd47a404
No known key found for this signature in database
17 changed files with 312 additions and 136 deletions

View file

@ -1,3 +1,3 @@
{ {
"cSpell.words": ["Appbar", "Displayname"] "cSpell.words": ["Appbar", "Displayname", "prefs"]
} }

View file

@ -11,6 +11,7 @@ import "package:nexus/models/session_backup.dart";
class ClientController extends AsyncNotifier<Client> { class ClientController extends AsyncNotifier<Client> {
static const sessionBackupKey = "sessionBackup"; static const sessionBackupKey = "sessionBackup";
@override @override
Future<Client> build() async { Future<Client> build() async {
if (!voz.isInitialized()) await voz_fl.init(); if (!voz.isInitialized()) await voz_fl.init();
@ -43,10 +44,6 @@ class ClientController extends AsyncNotifier<Client> {
); );
} }
ref.onDispose(
client.onRoomState.stream.listen((_) => ref.notifyListeners()).cancel,
);
return client; return client;
} }

View file

@ -1,24 +0,0 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/helpers/extensions/get_full_room.dart";
import "package:nexus/models/full_room.dart";
class CurrentRoomController extends AsyncNotifier<FullRoom?> {
@override
Future<FullRoom?> build() async {
final spaces = await ref.watch(SpacesController.provider.future);
if (spaces.isEmpty || spaces[0].children.isEmpty) return null;
return spaces[0].children[0].roomData.fullRoom;
}
Future<void> set(FullRoom room) async {
await future;
state = AsyncValue.data(room);
}
static final provider =
AsyncNotifierProvider<CurrentRoomController, FullRoom?>(
CurrentRoomController.new,
);
}

View file

@ -0,0 +1,30 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/shared_prefs_controller.dart";
class KeyController extends Notifier<String?> {
final String key;
KeyController(this.key);
static const String spaceKey = "space";
static const String roomKey = "room";
@override
String? build() =>
ref.watch(SharedPrefsController.provider).requireValue.getString(key);
Future<void> set(String? id) async {
final prefs = ref.watch(SharedPrefsController.provider).requireValue;
state = id;
if (id == null) {
prefs.remove(key);
} else {
prefs.setString(key, id);
}
}
static final provider =
NotifierProvider.family<KeyController, String?, String>(
KeyController.new,
);
}

View file

@ -1,6 +1,6 @@
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/current_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/helpers/extensions/event_to_message.dart";
class MessageController extends AsyncNotifier<TextMessage?> { class MessageController extends AsyncNotifier<TextMessage?> {
@ -9,11 +9,11 @@ class MessageController extends AsyncNotifier<TextMessage?> {
@override @override
Future<TextMessage?> build() async { Future<TextMessage?> build() async {
final room = await ref.watch(CurrentRoomController.provider.future); final room = await ref.watch(SelectedRoomController.provider.future);
if (room == null) return null; if (room == null) return null;
final event = await room.roomData.getEventById(id); final event = await room.roomData.getEventById(id);
return (await event?.toMessage(mustBeText: true)) as TextMessage; return (await event?.toMessage(mustBeText: true)) as TextMessage?;
} }
static final provider = static final provider =

View file

@ -1,12 +1,33 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/models/full_room.dart";
class SelectedRoomController extends Notifier<int> { class SelectedRoomController extends AsyncNotifier<FullRoom?> {
@override @override
int build() => 0; bool updateShouldNotify(
AsyncValue<FullRoom?> previous,
AsyncValue<FullRoom?> next,
) =>
previous.value?.avatar != next.value?.avatar ||
previous.value?.title != next.value?.title;
void set(int value) => state = value; @override
Future<FullRoom?> build() async {
final space = await ref.watch(SelectedSpaceController.provider.future);
final selectedRoomId = ref.watch(
KeyController.provider(KeyController.roomKey),
);
static final provider = NotifierProvider<SelectedRoomController, int>( return space.children.firstWhereOrNull(
SelectedRoomController.new, (room) => room.roomData.id == selectedRoomId,
); ) ??
space.children.firstOrNull;
}
static final provider =
AsyncNotifierProvider<SelectedRoomController, FullRoom?>(
SelectedRoomController.new,
);
} }

View file

@ -1,12 +1,22 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/models/space.dart";
class SelectedSpaceController extends Notifier<int> { class SelectedSpaceController extends AsyncNotifier<Space> {
@override @override
int build() => 0; Future<Space> build() async {
final spaces = await ref.watch(SpacesController.provider.future);
final selectedSpaceId = ref.watch(
KeyController.provider(KeyController.spaceKey),
);
void set(int value) => state = value; return spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ??
spaces.first;
}
static final provider = NotifierProvider<SelectedSpaceController, int>( static final provider = AsyncNotifierProvider<SelectedSpaceController, Space>(
SelectedSpaceController.new, SelectedSpaceController.new,
); );
} }

View file

@ -0,0 +1,12 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:shared_preferences/shared_preferences.dart";
class SharedPrefsController extends AsyncNotifier<SharedPreferences> {
@override
Future<SharedPreferences> build() => SharedPreferences.getInstance();
static final provider =
AsyncNotifierProvider<SharedPrefsController, SharedPreferences>(
SharedPrefsController.new,
);
}

View file

@ -11,6 +11,10 @@ class SpacesController extends AsyncNotifier<IList<Space>> {
Future<IList<Space>> build() async { Future<IList<Space>> build() async {
final client = await ref.watch(ClientController.provider.future); final client = await ref.watch(ClientController.provider.future);
ref.onDispose(
client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel,
);
final topLevel = await Future.wait( final topLevel = await Future.wait(
client.rooms client.rooms
.where((room) => !room.isDirectChat) .where((room) => !room.isDirectChat)
@ -33,12 +37,14 @@ class SpacesController extends AsyncNotifier<IList<Space>> {
Space( Space(
client: client, client: client,
title: "Home", title: "Home",
id: "home",
children: topLevelRooms, children: topLevelRooms,
icon: Icon(Icons.home), icon: Icon(Icons.home),
), ),
Space( Space(
client: client, client: client,
title: "Direct Messages", title: "Direct Messages",
id: "dms",
children: await Future.wait( children: await Future.wait(
client.rooms client.rooms
.where((room) => room.isDirectChat) .where((room) => room.isDirectChat)
@ -52,6 +58,7 @@ class SpacesController extends AsyncNotifier<IList<Space>> {
client: client, client: client,
title: space.title, title: space.title,
avatar: space.avatar, avatar: space.avatar,
id: space.roomData.id,
roomData: space.roomData, roomData: space.roomData,
children: await Future.wait( children: await Future.wait(
space.roomData.spaceChildren space.roomData.spaceChildren

View file

@ -1,3 +1,4 @@
import "package:flutter/foundation.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:matrix/matrix.dart"; import "package:matrix/matrix.dart";
@ -28,7 +29,7 @@ extension EventToMessage on Event {
? this.eventId ? this.eventId
: relationshipEventId ?? this.eventId; : relationshipEventId ?? this.eventId;
if (redacted) return null; if (redacted && !mustBeText) return null;
final asText = final asText =
Message.text( Message.text(
@ -88,13 +89,15 @@ extension EventToMessage on Event {
"${senderFromMemoryOrFallback.calcDisplayname()} joined the room.", "${senderFromMemoryOrFallback.calcDisplayname()} joined the room.",
), ),
EventTypes.Redaction => null, EventTypes.Redaction => null,
EventTypes.Reaction => null, _ =>
_ => Message.unsupported( kDebugMode
metadata: metadata, ? Message.unsupported(
id: eventId, metadata: metadata,
authorId: senderId, id: eventId,
replyToMessageId: replyId, authorId: senderId,
), replyToMessageId: replyId,
)
: null,
}; };
} }
} }

View file

@ -1,14 +1,35 @@
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/shared_prefs_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/pages/chat_page.dart"; import "package:nexus/pages/chat_page.dart";
import "package:nexus/pages/login_page.dart"; import "package:nexus/pages/login_page.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:window_manager/window_manager.dart"; import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:dynamic_system_colors/dynamic_system_colors.dart"; import "package:dynamic_system_colors/dynamic_system_colors.dart";
import "package:window_size/window_size.dart"; import "package:window_size/window_size.dart";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void showError(Object error, [StackTrace? stackTrace]) {
if (error.toString().contains("DioException")) return;
if (error.toString().contains("UTF-16")) return;
debugPrintStack(stackTrace: stackTrace, label: error.toString());
if (navigatorKey.currentContext != null) {
Future.delayed(
Duration.zero,
() => showDialog(
context: navigatorKey.currentContext!,
builder: (_) => ErrorDialog(error, stackTrace),
barrierDismissible: false,
),
);
}
}
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -17,6 +38,9 @@ void main() async {
WindowOptions(titleBarStyle: TitleBarStyle.hidden), WindowOptions(titleBarStyle: TitleBarStyle.hidden),
); );
FlutterError.onError = (FlutterErrorDetails details) =>
showError(details.exception.toString(), details.stack);
setWindowMinSize(const Size.square(500)); setWindowMinSize(const Size.square(500));
runApp(ProviderScope(child: const App())); runApp(ProviderScope(child: const App()));
@ -28,6 +52,7 @@ class App extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder( Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => MaterialApp( builder: (lightDynamic, darkDynamic) => MaterialApp(
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
// Use indigo to work around bugs in theme generation // Use indigo to work around bugs in theme generation
theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo)) theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo))
@ -40,10 +65,14 @@ class App extends ConsumerWidget {
)) ))
.theme, .theme,
home: ref home: ref
.watch(ClientController.provider) .watch(SharedPrefsController.provider)
.betterWhen( .betterWhen(
data: (client) => data: (_) => ref
client.accessToken == null ? LoginPage() : ChatPage(), .watch(ClientController.provider)
.betterWhen(
data: (client) =>
client.accessToken == null ? LoginPage() : ChatPage(),
),
), ),
), ),
); );

View file

@ -8,6 +8,7 @@ part "space.freezed.dart";
abstract class Space with _$Space { abstract class Space with _$Space {
const factory Space({ const factory Space({
required String title, required String title,
required String id,
required List<FullRoom> children, required List<FullRoom> children,
required Client client, required Client client,
Room? roomData, Room? roomData,

View file

@ -1,4 +1,3 @@
import "package:flutter/foundation.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_chat_ui/flutter_chat_ui.dart"; import "package:flutter_chat_ui/flutter_chat_ui.dart";
@ -9,7 +8,7 @@ import "package:flyer_chat_image_message/flyer_chat_image_message.dart";
import "package:flyer_chat_system_message/flyer_chat_system_message.dart"; import "package:flyer_chat_system_message/flyer_chat_system_message.dart";
import "package:flyer_chat_text_message/flyer_chat_text_message.dart"; import "package:flyer_chat_text_message/flyer_chat_text_message.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/current_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/room_chat_controller.dart"; import "package:nexus/controllers/room_chat_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/get_headers.dart";
@ -38,7 +37,7 @@ class RoomChat extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return ref return ref
.watch(CurrentRoomController.provider) .watch(SelectedRoomController.provider)
.betterWhen( .betterWhen(
data: (room) { data: (room) {
if (room == null) { if (room == null) {
@ -291,13 +290,12 @@ class RoomChat extends HookConsumerWidget {
index, { index, {
required bool isSentByMe, required bool isSentByMe,
MessageGroupStatus? groupStatus, MessageGroupStatus? groupStatus,
}) => kDebugMode }) => Text(
? Text( "${message.authorId} sent ${message.metadata?["eventType"]}",
"${message.authorId} sent ${message.metadata?["eventType"]}", style: theme.textTheme.labelSmall?.copyWith(
style: theme.textTheme.labelSmall color: Colors.grey,
?.copyWith(color: Colors.grey), ),
) ),
: SizedBox.shrink(),
), ),
onMessageSend: (message) { onMessageSend: (message) {
notifier.send( notifier.send(

View file

@ -20,8 +20,32 @@ class RoomMenu extends StatelessWidget {
child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")),
), ),
PopupMenuItem( PopupMenuItem(
onTap: () => onTap: () => showDialog(
showDialog(context: context, builder: (context) => AlertDialog()), context: context,
builder: (context) => AlertDialog(
title: Text("Leave Room"),
content: Text(
"Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?",
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text("Cancel"),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final snackbar = ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Leaving room...")));
await room.leave();
snackbar.close();
},
child: Text("Leave"),
),
],
),
),
child: ListTile( child: ListTile(
leading: Icon(Icons.logout, color: danger), leading: Icon(Icons.logout, color: danger),
title: Text("Leave", style: TextStyle(color: danger)), title: Text("Leave", style: TextStyle(color: danger)),

View file

@ -1,8 +1,7 @@
import "package:collection/collection.dart"; import "package:collection/collection.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/current_room_controller.dart"; import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/controllers/selected_space_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart";
import "package:nexus/controllers/spaces_controller.dart"; import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
@ -16,11 +15,15 @@ class Sidebar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final selectedSpaceProvider = SelectedSpaceController.provider; final selectedSpaceProvider = KeyController.provider(
KeyController.spaceKey,
);
final selectedSpace = ref.watch(selectedSpaceProvider); final selectedSpace = ref.watch(selectedSpaceProvider);
final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier);
final selectedRoomController = SelectedRoomController.provider; final selectedRoomController = KeyController.provider(
KeyController.roomKey,
);
final selectedRoom = ref.watch(selectedRoomController); final selectedRoom = ref.watch(selectedRoomController);
final selectedRoomNotifier = ref.watch(selectedRoomController.notifier); final selectedRoomNotifier = ref.watch(selectedRoomController.notifier);
@ -36,73 +39,87 @@ class Sidebar extends HookConsumerWidget {
debugPrintStack(label: error.toString(), stackTrace: stack); debugPrintStack(label: error.toString(), stackTrace: stack);
throw error; throw error;
}, },
data: (spaces) => NavigationRail( data: (spaces) {
scrollable: true, final indexOfSelected = spaces.indexWhere(
onDestinationSelected: (value) { (space) => space.id == selectedSpace,
selectedRoomNotifier.set(0); );
selectedSpaceNotifier.set(value); final selectedIndex = indexOfSelected == -1
ref ? null
.watch(CurrentRoomController.provider.notifier) : indexOfSelected;
.set(spaces[value].children[0]);
}, return NavigationRail(
destinations: spaces scrollable: true,
.map( onDestinationSelected: (value) {
(space) => NavigationRailDestination( selectedSpaceNotifier.set(spaces[value].roomData?.id);
icon: AvatarOrHash( selectedRoomNotifier.set(
space.avatar, spaces[value].children.firstOrNull?.roomData.id,
fallback: space.icon, );
space.title, },
headers: space.client.headers, destinations: spaces
hasBadge: .map(
space.children.firstWhereOrNull( (space) => NavigationRailDestination(
(room) => room.roomData.hasNewMessages, icon: AvatarOrHash(
) != space.avatar,
null, fallback: space.icon,
space.title,
headers: space.client.headers,
hasBadge:
space.children.firstWhereOrNull(
(room) => room.roomData.hasNewMessages,
) !=
null,
),
label: Text(space.title),
padding: EdgeInsets.only(top: 4),
), ),
label: Text(space.title), )
padding: EdgeInsets.only(top: 4), .toList(),
), selectedIndex: selectedIndex,
) trailingAtBottom: true,
.toList(), trailing: Padding(
selectedIndex: selectedSpace, padding: EdgeInsets.symmetric(vertical: 16),
trailingAtBottom: true, child: Column(
trailing: Padding( spacing: 8,
padding: EdgeInsets.symmetric(vertical: 16), children: [
child: Column( IconButton(
spacing: 8, onPressed: () => Navigator.of(context).push(
children: [ // TODO: join or create room/space
IconButton( MaterialPageRoute(builder: (_) => SettingsPage()),
onPressed: () => Navigator.of(context).push( ),
// TODO: join or create room/space icon: Icon(Icons.add),
MaterialPageRoute(builder: (_) => SettingsPage()),
), ),
icon: Icon(Icons.add), IconButton(
), onPressed: () => Navigator.of(context).push(
IconButton( // TODO: explore public rooms/spaces
onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => SettingsPage()),
// TODO: explore public rooms/spaces ),
MaterialPageRoute(builder: (_) => SettingsPage()), icon: Icon(Icons.explore),
), ),
icon: Icon(Icons.explore), IconButton(
), onPressed: () => Navigator.of(context).push(
IconButton( // TODO: explore public rooms/spaces
onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => SettingsPage()),
// TODO: explore public rooms/spaces ),
MaterialPageRoute(builder: (_) => SettingsPage()), icon: Icon(Icons.settings),
), ),
icon: Icon(Icons.settings), ],
), ),
],
), ),
), );
), },
), ),
Expanded( Expanded(
child: ref child: ref
.watch(SpacesController.provider) .watch(SelectedSpaceController.provider)
.betterWhen( .betterWhen(
data: (spaces) { data: (space) {
final space = spaces[selectedSpace]; final indexOfSelected = space.children.indexWhere(
(room) => room.roomData.id == selectedRoom,
);
final selectedIndex = indexOfSelected == -1
? null
: indexOfSelected;
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
appBar: AppBar( appBar: AppBar(
@ -125,9 +142,7 @@ class Sidebar extends HookConsumerWidget {
scrollable: true, scrollable: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
extended: true, extended: true,
selectedIndex: space.children.isEmpty selectedIndex: selectedIndex,
? null
: selectedRoom,
destinations: space.children destinations: space.children
.map( .map(
(room) => NavigationRailDestination( (room) => NavigationRailDestination(
@ -144,12 +159,8 @@ class Sidebar extends HookConsumerWidget {
), ),
) )
.toList(), .toList(),
onDestinationSelected: (value) { onDestinationSelected: (value) => selectedRoomNotifier
selectedRoomNotifier.set(value); .set(space.children[value].roomData.id),
ref
.watch(CurrentRoomController.provider.notifier)
.set(space.children[value]);
},
), ),
); );
}, },

View file

@ -1200,6 +1200,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:

View file

@ -64,6 +64,7 @@ dependencies:
json_annotation: ^4.9.0 json_annotation: ^4.9.0
vodozemac: ^0.4.0 vodozemac: ^0.4.0
clipboard: ^2.0.2 clipboard: ^2.0.2
shared_preferences: ^2.5.3
dev_dependencies: dev_dependencies:
build_runner: ^2.4.11 build_runner: ^2.4.11