diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 4970201..f317c42 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,4 +1,3 @@ -import "dart:io"; import "package:flutter/foundation.dart"; import "package:matrix/matrix.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -13,6 +12,7 @@ class ClientController extends AsyncNotifier { "nexus", logLevel: kReleaseMode ? Level.warning : Level.verbose, importantStateEvents: {"im.ponies.room_emotes"}, + supportedLoginTypes: {AuthenticationTypes.password}, database: await MatrixSdkDatabase.init( "nexus", database: await databaseFactoryFfi.openDatabase( @@ -22,18 +22,18 @@ class ClientController extends AsyncNotifier { ); // TODO: Save info - if (client.homeserver == null) { - await client.checkHomeserver(Uri.https("federated.nexus")); - } - if (client.accessToken == null) { - await client.login( - LoginType.mLoginPassword, - initialDeviceDisplayName: "Nexus Client", - deviceId: "temp", // TODO - identifier: AuthenticationUserIdentifier(user: "quadradical"), - password: File("./password.txt").readAsStringSync(), - ); - } + // if (client.homeserver == null) { + // await client.checkHomeserver(Uri.https("federated.nexus")); + // } + // if (client.accessToken == null) { + // await client.login( + // LoginType.mLoginPassword, + // initialDeviceDisplayName: "Nexus Client", + // deviceId: "temp", // TODO + // identifier: AuthenticationUserIdentifier(user: "quadradical"), + // password: File("./password.txt").readAsStringSync(), + // ); + // } return client; } diff --git a/lib/helpers/extension_helper.dart b/lib/helpers/extension_helper.dart index 2ee41f4..b2287eb 100644 --- a/lib/helpers/extension_helper.dart +++ b/lib/helpers/extension_helper.dart @@ -123,9 +123,13 @@ extension ToMessage on Event { extension ToTheme on ColorScheme { ThemeData get theme => ThemeData.from(colorScheme: this).copyWith( + cardTheme: CardThemeData(color: primaryContainer), appBarTheme: AppBarTheme( titleSpacing: 0, backgroundColor: surfaceContainerLow, ), + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), ); } diff --git a/lib/helpers/homeserver_helper.dart b/lib/helpers/homeserver_helper.dart new file mode 100644 index 0000000..bba49c8 --- /dev/null +++ b/lib/helpers/homeserver_helper.dart @@ -0,0 +1,19 @@ +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; + +class HomeserverHelper { + final Ref ref; + HomeserverHelper(this.ref); + + Future setHomeserver(Uri homeserverUrl) async { + final client = await ref.watch(ClientController.provider.future); + try { + await client.checkHomeserver(homeserverUrl); + return true; + } catch (_) { + return false; + } + } + + static final provider = Provider(HomeserverHelper.new); +} diff --git a/lib/main.dart b/lib/main.dart index 6d6bc14..b15db1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,8 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_controller.dart"; import "package:nexus/helpers/extension_helper.dart"; -import "package:nexus/widgets/room_chat.dart"; -import "package:nexus/widgets/sidebar.dart"; +import "package:nexus/pages/home_page.dart"; +import "package:nexus/pages/homeserver_page.dart"; import "package:scaled_app/scaled_app.dart"; import "package:window_manager/window_manager.dart"; import "package:flutter/material.dart"; @@ -21,46 +22,28 @@ void main() async { runApp(ProviderScope(child: const App())); } -class App extends StatelessWidget { +class App extends ConsumerWidget { const App({super.key}); @override - Widget build(BuildContext context) => DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) => LayoutBuilder( - builder: (context, constraints) { - final isDesktop = constraints.maxWidth > 650; - final showMembersByDefault = constraints.maxWidth > 1000; - - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: - (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo)) - .theme, - darkTheme: - (darkDynamic ?? - ColorScheme.fromSeed( - seedColor: Colors.indigo, - brightness: Brightness.dark, - )) - .theme, - home: Scaffold( - body: Builder( - builder: (context) => Row( - children: [ - if (isDesktop) Sidebar(), - Expanded( - child: RoomChat( - isDesktop: isDesktop, - showMembersByDefault: showMembersByDefault, - ), - ), - ], - ), - ), - drawer: isDesktop ? null : Sidebar(), + Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) => MaterialApp( + debugShowCheckedModeBanner: false, + theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo)) + .theme, + darkTheme: + (darkDynamic ?? + ColorScheme.fromSeed( + seedColor: Colors.indigo, + brightness: Brightness.dark, + )) + .theme, + home: ref + .watch(ClientController.provider) + .betterWhen( + data: (client) => + client.accessToken == null ? HomeserverPage() : HomePage(), ), - ); - }, ), ); } diff --git a/lib/models/homeserver.dart b/lib/models/homeserver.dart new file mode 100644 index 0000000..903e23d --- /dev/null +++ b/lib/models/homeserver.dart @@ -0,0 +1,12 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "homeserver.freezed.dart"; + +@freezed +abstract class Homeserver with _$Homeserver { + const factory Homeserver({ + required String name, + required String description, + required Uri url, + required String iconUrl, + }) = _Homeserver; +} diff --git a/lib/models/homeserver.freezed.dart b/lib/models/homeserver.freezed.dart new file mode 100644 index 0000000..5ca7760 --- /dev/null +++ b/lib/models/homeserver.freezed.dart @@ -0,0 +1,280 @@ +// 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 'homeserver.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$Homeserver { + + String get name; String get description; Uri get url; String get iconUrl; +/// Create a copy of Homeserver +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$HomeserverCopyWith get copyWith => _$HomeserverCopyWithImpl(this as Homeserver, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Homeserver&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)); +} + + +@override +int get hashCode => Object.hash(runtimeType,name,description,url,iconUrl); + +@override +String toString() { + return 'Homeserver(name: $name, description: $description, url: $url, iconUrl: $iconUrl)'; +} + + +} + +/// @nodoc +abstract mixin class $HomeserverCopyWith<$Res> { + factory $HomeserverCopyWith(Homeserver value, $Res Function(Homeserver) _then) = _$HomeserverCopyWithImpl; +@useResult +$Res call({ + String name, String description, Uri url, String iconUrl +}); + + + + +} +/// @nodoc +class _$HomeserverCopyWithImpl<$Res> + implements $HomeserverCopyWith<$Res> { + _$HomeserverCopyWithImpl(this._self, this._then); + + final Homeserver _self; + final $Res Function(Homeserver) _then; + +/// Create a copy of Homeserver +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? description = null,Object? url = null,Object? iconUrl = null,}) { + return _then(_self.copyWith( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable +as Uri,iconUrl: null == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Homeserver]. +extension HomeserverPatterns on Homeserver { +/// 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 Function( _Homeserver value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Homeserver() 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 Function( _Homeserver value) $default,){ +final _that = this; +switch (_that) { +case _Homeserver(): +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? Function( _Homeserver value)? $default,){ +final _that = this; +switch (_that) { +case _Homeserver() 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 Function( String name, String description, Uri url, String iconUrl)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Homeserver() when $default != null: +return $default(_that.name,_that.description,_that.url,_that.iconUrl);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 Function( String name, String description, Uri url, String iconUrl) $default,) {final _that = this; +switch (_that) { +case _Homeserver(): +return $default(_that.name,_that.description,_that.url,_that.iconUrl);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? Function( String name, String description, Uri url, String iconUrl)? $default,) {final _that = this; +switch (_that) { +case _Homeserver() when $default != null: +return $default(_that.name,_that.description,_that.url,_that.iconUrl);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _Homeserver implements Homeserver { + const _Homeserver({required this.name, required this.description, required this.url, required this.iconUrl}); + + +@override final String name; +@override final String description; +@override final Uri url; +@override final String iconUrl; + +/// Create a copy of Homeserver +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$HomeserverCopyWith<_Homeserver> get copyWith => __$HomeserverCopyWithImpl<_Homeserver>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Homeserver&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.url, url) || other.url == url)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)); +} + + +@override +int get hashCode => Object.hash(runtimeType,name,description,url,iconUrl); + +@override +String toString() { + return 'Homeserver(name: $name, description: $description, url: $url, iconUrl: $iconUrl)'; +} + + +} + +/// @nodoc +abstract mixin class _$HomeserverCopyWith<$Res> implements $HomeserverCopyWith<$Res> { + factory _$HomeserverCopyWith(_Homeserver value, $Res Function(_Homeserver) _then) = __$HomeserverCopyWithImpl; +@override @useResult +$Res call({ + String name, String description, Uri url, String iconUrl +}); + + + + +} +/// @nodoc +class __$HomeserverCopyWithImpl<$Res> + implements _$HomeserverCopyWith<$Res> { + __$HomeserverCopyWithImpl(this._self, this._then); + + final _Homeserver _self; + final $Res Function(_Homeserver) _then; + +/// Create a copy of Homeserver +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? description = null,Object? url = null,Object? iconUrl = null,}) { + return _then(_Homeserver( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable +as Uri,iconUrl: null == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..64a326f --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,32 @@ +import "package:flutter/material.dart"; +import "package:nexus/widgets/room_chat.dart"; +import "package:nexus/widgets/sidebar.dart"; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) { + final isDesktop = constraints.maxWidth > 650; + final showMembersByDefault = constraints.maxWidth > 1000; + + return Scaffold( + body: Builder( + builder: (context) => Row( + children: [ + if (isDesktop) Sidebar(), + Expanded( + child: RoomChat( + isDesktop: isDesktop, + showMembersByDefault: showMembersByDefault, + ), + ), + ], + ), + ), + drawer: isDesktop ? null : Sidebar(), + ); + }, + ); +} diff --git a/lib/pages/homeserver_page.dart b/lib/pages/homeserver_page.dart new file mode 100644 index 0000000..627ebef --- /dev/null +++ b/lib/pages/homeserver_page.dart @@ -0,0 +1,172 @@ +import "dart:io"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/helpers/homeserver_helper.dart"; +import "package:nexus/helpers/launch_helper.dart"; +import "package:nexus/models/homeserver.dart"; +import "package:nexus/pages/login_page.dart"; +import "package:nexus/widgets/appbar.dart"; +import "package:nexus/widgets/divider_text.dart"; + +class HomeserverPage extends HookConsumerWidget { + const HomeserverPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isChecking = useState(false); + Future setHomeserver(Uri? homeserver) async { + isChecking.value = true; + final messenger = ScaffoldMessenger.of(context); + final snackbar = messenger.showSnackBar( + SnackBar( + content: Text("Checking homeserver..."), + duration: Duration(days: 1), + ), + ); + final succeeded = homeserver == null + ? false + : await ref + .watch(HomeserverHelper.provider) + .setHomeserver( + homeserver.hasScheme + ? homeserver + : Uri.https(homeserver.path), + ); + + snackbar.close(); + if (context.mounted) { + if (succeeded) { + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => LoginPage())); + } else { + messenger.showSnackBar( + SnackBar( + content: Text( + "Homeserver verification failed. Is your homeserver down?", + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + backgroundColor: Theme.of(context).colorScheme.errorContainer, + ), + ); + } + } + isChecking.value = false; + } + + final homeserverUrl = useTextEditingController(); + + return Scaffold( + appBar: Appbar(backgroundColor: Colors.transparent), + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints.tight(Size.fromWidth(500)), + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 32), + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/icon.svg"), + SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nexus", + style: Theme.of(context).textTheme.displayMedium, + ), + Text( + "A Simple Matrix Client", + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ], + ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 12), + child: Divider(), + ), + + DividerText("Enter a homeserver domain:"), + Row( + spacing: 8, + children: [ + Expanded( + child: TextField( + controller: homeserverUrl, + decoration: InputDecoration( + labelText: "Homeserver URL (e.g. matrix.org)", + ), + ), + ), + IconButton.filled( + onPressed: isChecking.value + ? null + : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), + icon: Icon(Icons.check), + ), + ], + ), + DividerText("Or, choose from some popular homeservers:"), + + ...([ + Homeserver( + name: "Matrix.org", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + url: Uri.https("matrix.org"), + iconUrl: + "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", + ), + Homeserver( + name: "Federated Nexus", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + url: Uri.https("federated.nexus"), + iconUrl: "https://federated.nexus/images/icon.png", + ), + Homeserver( + name: "envs.net", + description: + "envs.net is a minimalist, non-commercial shared linux system and will always be free to use.", + url: Uri.https("envs.net"), + iconUrl: "https://envs.net/favicon.ico", + ), + ].map( + (homeserver) => Card( + child: ListTile( + title: Text(homeserver.name), + leading: Image.network(homeserver.iconUrl, height: 32), + subtitle: Text(homeserver.description), + onTap: isChecking.value + ? null + : () => setHomeserver(homeserver.url), + trailing: IconButton( + onPressed: () => ref + .watch(LaunchHelper.provider) + .launchUrl(homeserver.url), + icon: Icon(Icons.info_outline), + ), + ), + ), + )), + SizedBox(height: 8), + TextButton( + onPressed: () => ref + .watch(LaunchHelper.provider) + .launchUrl(Uri.https("servers.joinmatrix.org")), + child: Text("See more homeservers..."), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..2c013e3 --- /dev/null +++ b/lib/pages/login_page.dart @@ -0,0 +1,11 @@ +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/widgets/appbar.dart"; + +class LoginPage extends HookConsumerWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) => + Scaffold(appBar: Appbar()); +} diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart new file mode 100644 index 0000000..3ecaa1d --- /dev/null +++ b/lib/widgets/appbar.dart @@ -0,0 +1,35 @@ +import "dart:io"; +import "package:flutter/material.dart"; + +class Appbar extends StatelessWidget implements PreferredSizeWidget { + final Widget? leading; + final Widget? title; + final Color? backgroundColor; + final double? scrolledUnderElevation; + final List actions; + const Appbar({ + super.key, + this.title, + this.backgroundColor, + this.scrolledUnderElevation, + this.leading, + this.actions = const [], + }); + + @override + Size get preferredSize => AppBar().preferredSize; + + @override + AppBar build(BuildContext context) => AppBar( + leading: leading, + backgroundColor: backgroundColor, + scrolledUnderElevation: scrolledUnderElevation, + actionsPadding: EdgeInsets.symmetric(horizontal: 8), + title: title, + actions: [ + ...actions, + if (!(Platform.isAndroid || Platform.isIOS)) + IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)), + ], + ); +} diff --git a/lib/widgets/divider_text.dart b/lib/widgets/divider_text.dart new file mode 100644 index 0000000..ca78844 --- /dev/null +++ b/lib/widgets/divider_text.dart @@ -0,0 +1,29 @@ +import "package:flutter/material.dart"; + +class DividerText extends StatelessWidget { + final String text; + + const DividerText(this.text, {super.key}); + + @override + Widget build(BuildContext context) => LayoutBuilder( + builder: (context, constraints) => Row( + children: [ + SizedBox( + width: 16, + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: constraints.maxWidth - 32), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(text, style: Theme.of(context).textTheme.labelLarge), + ), + ), + Expanded( + child: Divider(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ); +} diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 289757b..e61b43c 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -1,8 +1,7 @@ -import "dart:io"; - import "package:flutter/material.dart"; import "package:nexus/helpers/extension_helper.dart"; import "package:nexus/models/full_room.dart"; +import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { @@ -22,7 +21,7 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => AppBar().preferredSize; @override - AppBar build(BuildContext context) => AppBar( + Widget build(BuildContext context) => Appbar( leading: isDesktop ? AvatarOrHash( room.avatar, @@ -33,7 +32,6 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { ) : DrawerButton(onPressed: () => onOpenDrawer(context)), scrolledUnderElevation: 0, - actionsPadding: EdgeInsets.symmetric(horizontal: 8), title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -54,8 +52,6 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget { onPressed: () => onOpenMemberList(context), icon: Icon(Icons.people), ), - if (!(Platform.isAndroid || Platform.isIOS)) - IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)), ], ); } diff --git a/pubspec.lock b/pubspec.lock index a1b17b0..b370b4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -565,7 +565,7 @@ packages: source: hosted version: "2.11.1" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355" diff --git a/pubspec.yaml b/pubspec.yaml index 44bfd0c..9cbe9ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,8 @@ version: 1.0.0 publish_to: none flutter: + assets: + - assets/ uses-material-design: true environment: @@ -59,6 +61,7 @@ dependencies: flutter_vodozemac: ^0.4.1 flutter_widget_from_html_core: ^0.17.0 gpt_markdown: ^1.1.4 + flutter_svg: ^2.2.2 dev_dependencies: build_runner: ^2.4.11