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:flutter/foundation.dart";
|
||||||
import "package:matrix/matrix.dart";
|
import "package:matrix/matrix.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
|
@ -13,6 +12,7 @@ class ClientController extends AsyncNotifier<Client> {
|
||||||
"nexus",
|
"nexus",
|
||||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||||
importantStateEvents: {"im.ponies.room_emotes"},
|
importantStateEvents: {"im.ponies.room_emotes"},
|
||||||
|
supportedLoginTypes: {AuthenticationTypes.password},
|
||||||
database: await MatrixSdkDatabase.init(
|
database: await MatrixSdkDatabase.init(
|
||||||
"nexus",
|
"nexus",
|
||||||
database: await databaseFactoryFfi.openDatabase(
|
database: await databaseFactoryFfi.openDatabase(
|
||||||
|
|
@ -22,18 +22,18 @@ class ClientController extends AsyncNotifier<Client> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Save info
|
// TODO: Save info
|
||||||
if (client.homeserver == null) {
|
// if (client.homeserver == null) {
|
||||||
await client.checkHomeserver(Uri.https("federated.nexus"));
|
// await client.checkHomeserver(Uri.https("federated.nexus"));
|
||||||
}
|
// }
|
||||||
if (client.accessToken == null) {
|
// if (client.accessToken == null) {
|
||||||
await client.login(
|
// await client.login(
|
||||||
LoginType.mLoginPassword,
|
// LoginType.mLoginPassword,
|
||||||
initialDeviceDisplayName: "Nexus Client",
|
// initialDeviceDisplayName: "Nexus Client",
|
||||||
deviceId: "temp", // TODO
|
// deviceId: "temp", // TODO
|
||||||
identifier: AuthenticationUserIdentifier(user: "quadradical"),
|
// identifier: AuthenticationUserIdentifier(user: "quadradical"),
|
||||||
password: File("./password.txt").readAsStringSync(),
|
// password: File("./password.txt").readAsStringSync(),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,13 @@ extension ToMessage on Event {
|
||||||
|
|
||||||
extension ToTheme on ColorScheme {
|
extension ToTheme on ColorScheme {
|
||||||
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
ThemeData get theme => ThemeData.from(colorScheme: this).copyWith(
|
||||||
|
cardTheme: CardThemeData(color: primaryContainer),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
backgroundColor: surfaceContainerLow,
|
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:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
import "package:nexus/widgets/room_chat.dart";
|
import "package:nexus/pages/home_page.dart";
|
||||||
import "package:nexus/widgets/sidebar.dart";
|
import "package:nexus/pages/homeserver_page.dart";
|
||||||
import "package:scaled_app/scaled_app.dart";
|
import "package:scaled_app/scaled_app.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";
|
||||||
|
|
@ -21,20 +22,14 @@ void main() async {
|
||||||
runApp(ProviderScope(child: const App()));
|
runApp(ProviderScope(child: const App()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends ConsumerWidget {
|
||||||
const App({super.key});
|
const App({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => DynamicColorBuilder(
|
Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) => LayoutBuilder(
|
builder: (lightDynamic, darkDynamic) => MaterialApp(
|
||||||
builder: (context, constraints) {
|
|
||||||
final isDesktop = constraints.maxWidth > 650;
|
|
||||||
final showMembersByDefault = constraints.maxWidth > 1000;
|
|
||||||
|
|
||||||
return MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme:
|
theme: (lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo))
|
||||||
(lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.indigo))
|
|
||||||
.theme,
|
.theme,
|
||||||
darkTheme:
|
darkTheme:
|
||||||
(darkDynamic ??
|
(darkDynamic ??
|
||||||
|
|
@ -43,24 +38,12 @@ class App extends StatelessWidget {
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
))
|
))
|
||||||
.theme,
|
.theme,
|
||||||
home: Scaffold(
|
home: ref
|
||||||
body: Builder(
|
.watch(ClientController.provider)
|
||||||
builder: (context) => Row(
|
.betterWhen(
|
||||||
children: [
|
data: (client) =>
|
||||||
if (isDesktop) Sidebar(),
|
client.accessToken == null ? HomeserverPage() : HomePage(),
|
||||||
Expanded(
|
|
||||||
child: RoomChat(
|
|
||||||
isDesktop: isDesktop,
|
|
||||||
showMembersByDefault: showMembersByDefault,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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:flutter/material.dart";
|
||||||
import "package:nexus/helpers/extension_helper.dart";
|
import "package:nexus/helpers/extension_helper.dart";
|
||||||
import "package:nexus/models/full_room.dart";
|
import "package:nexus/models/full_room.dart";
|
||||||
|
import "package:nexus/widgets/appbar.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
|
@ -22,7 +21,7 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
Size get preferredSize => AppBar().preferredSize;
|
Size get preferredSize => AppBar().preferredSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AppBar build(BuildContext context) => AppBar(
|
Widget build(BuildContext context) => Appbar(
|
||||||
leading: isDesktop
|
leading: isDesktop
|
||||||
? AvatarOrHash(
|
? AvatarOrHash(
|
||||||
room.avatar,
|
room.avatar,
|
||||||
|
|
@ -33,7 +32,6 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
)
|
)
|
||||||
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
: DrawerButton(onPressed: () => onOpenDrawer(context)),
|
||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -54,8 +52,6 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
onPressed: () => onOpenMemberList(context),
|
onPressed: () => onOpenMemberList(context),
|
||||||
icon: Icon(Icons.people),
|
icon: Icon(Icons.people),
|
||||||
),
|
),
|
||||||
if (!(Platform.isAndroid || Platform.isIOS))
|
|
||||||
IconButton(onPressed: () => exit(0), icon: Icon(Icons.close)),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -565,7 +565,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.1"
|
version: "2.11.1"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ version: 1.0.0
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -59,6 +61,7 @@ dependencies:
|
||||||
flutter_vodozemac: ^0.4.1
|
flutter_vodozemac: ^0.4.1
|
||||||
flutter_widget_from_html_core: ^0.17.0
|
flutter_widget_from_html_core: ^0.17.0
|
||||||
gpt_markdown: ^1.1.4
|
gpt_markdown: ^1.1.4
|
||||||
|
flutter_svg: ^2.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.11
|
build_runner: ^2.4.11
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue