start adding login flow
This commit is contained in:
parent
c0c4c02815
commit
c76a8f3c28
14 changed files with 634 additions and 58 deletions
|
|
@ -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<Client> {
|
|||
"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<Client> {
|
|||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
19
lib/helpers/homeserver_helper.dart
Normal file
19
lib/helpers/homeserver_helper.dart
Normal file
|
|
@ -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<bool> 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>(HomeserverHelper.new);
|
||||
}
|
||||
|
|
@ -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,20 +22,14 @@ 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(
|
||||
Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme:
|
||||
(lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo))
|
||||
theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo))
|
||||
.theme,
|
||||
darkTheme:
|
||||
(darkDynamic ??
|
||||
|
|
@ -43,24 +38,12 @@ class App extends StatelessWidget {
|
|||
brightness: Brightness.dark,
|
||||
))
|
||||
.theme,
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => Row(
|
||||
children: [
|
||||
if (isDesktop) Sidebar(),
|
||||
Expanded(
|
||||
child: RoomChat(
|
||||
isDesktop: isDesktop,
|
||||
showMembersByDefault: showMembersByDefault,
|
||||
home: ref
|
||||
.watch(ClientController.provider)
|
||||
.betterWhen(
|
||||
data: (client) =>
|
||||
client.accessToken == null ? HomeserverPage() : HomePage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
drawer: isDesktop ? null : Sidebar(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
12
lib/models/homeserver.dart
Normal file
12
lib/models/homeserver.dart
Normal file
|
|
@ -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;
|
||||
}
|
||||
280
lib/models/homeserver.freezed.dart
Normal file
280
lib/models/homeserver.freezed.dart
Normal file
|
|
@ -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>(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<Homeserver> get copyWith => _$HomeserverCopyWithImpl<Homeserver>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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
|
||||
32
lib/pages/home_page.dart
Normal file
32
lib/pages/home_page.dart
Normal file
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
172
lib/pages/homeserver_page.dart
Normal file
172
lib/pages/homeserver_page.dart
Normal file
|
|
@ -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<void> 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>[
|
||||
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..."),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/pages/login_page.dart
Normal file
11
lib/pages/login_page.dart
Normal file
|
|
@ -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());
|
||||
}
|
||||
35
lib/widgets/appbar.dart
Normal file
35
lib/widgets/appbar.dart
Normal file
|
|
@ -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<Widget> 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
29
lib/widgets/divider_text.dart
Normal file
29
lib/widgets/divider_text.dart
Normal file
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.11.1"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue