Account screen after logged in

This commit is contained in:
LittleSheep 2025-04-25 00:08:57 +08:00
parent d7d9e41db3
commit 057ab16381
14 changed files with 768 additions and 54 deletions

View File

@ -21,5 +21,6 @@
"nickname": "Nickname",
"email": "Email",
"fieldCannotBeEmpty": "This field cannot be empty.",
"fieldEmailAddressMustBeValid": "The email address must be valid."
"fieldEmailAddressMustBeValid": "The email address must be valid.",
"logout": "Logout"
}

View File

@ -1,13 +1,14 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/theme.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -48,13 +49,22 @@ void main() async {
final _appRouter = AppRouter();
class IslandApp extends ConsumerWidget {
class IslandApp extends HookConsumerWidget {
const IslandApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themeProvider);
useEffect(() {
final userNotifier = ref.read(userInfoProvider.notifier);
Future(() {
userNotifier.fetchUser();
print('user fetched');
});
return null;
}, []);
return MaterialApp.router(
theme: theme?.light,
darkTheme: theme?.dark,

42
lib/models/user.dart Normal file
View File

@ -0,0 +1,42 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class SnAccount with _$SnAccount {
const factory SnAccount({
required int id,
required String name,
required String nick,
required String language,
required bool isSuperuser,
required SnAccountProfile profile,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAccount;
factory SnAccount.fromJson(Map<String, dynamic> json) =>
_$SnAccountFromJson(json);
}
@freezed
abstract class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
required int id,
required String? firstName,
required String? middleName,
required String? lastName,
required String? bio,
required SnCloudFile? picture,
required SnCloudFile? background,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAccountProfile;
factory SnAccountProfile.fromJson(Map<String, dynamic> json) =>
_$SnAccountProfileFromJson(json);
}

View File

@ -0,0 +1,398 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// 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 'user.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnAccount {
int get id; String get name; String get nick; String get language; bool get isSuperuser; SnAccountProfile get profile; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount>(this as SnAccount, _$identity);
/// Serializes this SnAccount to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAccountCopyWith<$Res> {
factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl;
@useResult
$Res call({
int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnAccountProfileCopyWith<$Res> get profile;
}
/// @nodoc
class _$SnAccountCopyWithImpl<$Res>
implements $SnAccountCopyWith<$Res> {
_$SnAccountCopyWithImpl(this._self, this._then);
final SnAccount _self;
final $Res Function(SnAccount) _then;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountProfileCopyWith<$Res> get profile {
return $SnAccountProfileCopyWith<$Res>(_self.profile, (value) {
return _then(_self.copyWith(profile: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnAccount implements SnAccount {
const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.profile, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json);
@override final int id;
@override final String name;
@override final String nick;
@override final String language;
@override final bool isSuperuser;
@override final SnAccountProfile profile;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountCopyWith<_SnAccount> get copyWith => __$SnAccountCopyWithImpl<_SnAccount>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,profile,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Res> {
factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl;
@override @useResult
$Res call({
int id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnAccountProfileCopyWith<$Res> get profile;
}
/// @nodoc
class __$SnAccountCopyWithImpl<$Res>
implements _$SnAccountCopyWith<$Res> {
__$SnAccountCopyWithImpl(this._self, this._then);
final _SnAccount _self;
final $Res Function(_SnAccount) _then;
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccount(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable
as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable
as SnAccountProfile,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnAccount
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountProfileCopyWith<$Res> get profile {
return $SnAccountProfileCopyWith<$Res>(_self.profile, (value) {
return _then(_self.copyWith(profile: value));
});
}
}
/// @nodoc
mixin _$SnAccountProfile {
int get id; String? get firstName; String? get middleName; String? get lastName; String? get bio; SnCloudFile? get picture; SnCloudFile? get background; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCopyWithImpl<SnAccountProfile>(this as SnAccountProfile, _$identity);
/// Serializes this SnAccountProfile to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,picture,background,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult
$Res call({
int id, String? firstName, String? middleName, String? lastName, String? bio, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;
}
/// @nodoc
class _$SnAccountProfileCopyWithImpl<$Res>
implements $SnAccountProfileCopyWith<$Res> {
_$SnAccountProfileCopyWithImpl(this._self, this._then);
final SnAccountProfile _self;
final $Res Function(SnAccountProfile) _then;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = freezed,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: freezed == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, required this.bio, required this.picture, required this.background, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final int id;
@override final String? firstName;
@override final String? middleName;
@override final String? lastName;
@override final String? bio;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountProfileCopyWith<_SnAccountProfile> get copyWith => __$SnAccountProfileCopyWithImpl<_SnAccountProfile>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountProfileToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,firstName,middleName,lastName,bio,picture,background,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, picture: $picture, background: $background, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfileCopyWith<$Res> {
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult
$Res call({
int id, String? firstName, String? middleName, String? lastName, String? bio, SnCloudFile? picture, SnCloudFile? background, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;
}
/// @nodoc
class __$SnAccountProfileCopyWithImpl<$Res>
implements _$SnAccountProfileCopyWith<$Res> {
__$SnAccountProfileCopyWithImpl(this._self, this._then);
final _SnAccountProfile _self;
final $Res Function(_SnAccountProfile) _then;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = freezed,Object? picture = freezed,Object? background = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountProfile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as int,firstName: freezed == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
as String?,middleName: freezed == middleName ? _self.middleName : middleName // ignore: cast_nullable_to_non_nullable
as String?,lastName: freezed == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable
as String?,bio: freezed == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}
}
// dart format on

74
lib/models/user.g.dart Normal file
View File

@ -0,0 +1,74 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
nick: json['nick'] as String,
language: json['language'] as String,
isSuperuser: json['is_superuser'] as bool,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'nick': instance.nick,
'language': instance.language,
'is_superuser': instance.isSuperuser,
'profile': instance.profile.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: (json['id'] as num).toInt(),
firstName: json['first_name'] as String?,
middleName: json['middle_name'] as String?,
lastName: json['last_name'] as String?,
bio: json['bio'] as String?,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(
json['background'] as Map<String, dynamic>,
),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
<String, dynamic>{
'id': instance.id,
'first_name': instance.firstName,
'middle_name': instance.middleName,
'last_name': instance.lastName,
'bio': instance.bio,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

40
lib/pods/userinfo.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final Ref _ref;
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null));
Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString('dyn_user_atk');
}
Future<void> fetchUser() async {
state = const AsyncValue.loading();
try {
final client = _ref.read(apiClientProvider);
final response = await client.get('/accounts/me');
final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user);
} catch (error, stackTrace) {
print('Failed to fetch user: $error');
state = AsyncValue.error(error, stackTrace);
}
}
Future<void> logOut() async {
state = const AsyncValue.data(null);
final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
}
}
final userInfoProvider =
StateNotifierProvider<UserInfoNotifier, AsyncValue<SnAccount?>>(
(ref) => UserInfoNotifier(ref),
);

View File

@ -8,8 +8,17 @@ class AppRouter extends RootStackRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: ExploreRoute.page, path: '/'),
AutoRoute(page: AccountRoute.page, path: '/account'),
AutoRoute(
page: ExploreRoute.page,
path: '/',
meta: {'bottomNav': true},
initial: true,
),
AutoRoute(
page: AccountRoute.page,
path: '/account',
meta: {'bottomNav': true},
),
AutoRoute(page: LoginRoute.page, path: '/auth/login'),
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
];

View File

@ -1,12 +1,102 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()
class AccountScreen extends StatelessWidget {
class AccountScreen extends HookConsumerWidget {
const AccountScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userInfoProvider);
if (!user.hasValue) return _UnauthorizedAccountScreen();
return AppScaffold(
appBar: AppBar(title: const Text('Account')),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (user.value!.profile.background != null)
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: AspectRatio(
aspectRatio: 16 / 7,
child: CloudFileWidget(
item: user.value!.profile.background!,
fit: BoxFit.cover,
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: [
ProfilePictureWidget(
item: user.value!.profile.picture,
radius: 24,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(user.value!.nick).bold().fontSize(16),
Text('@${user.value!.name}'),
],
),
Text(
user.value!.profile.bio ?? 'No description yet.',
),
],
),
],
).padding(horizontal: 16, vertical: 16),
],
),
),
ListTile(
leading: const Icon(LucideIcons.logOut),
trailing: const Icon(LucideIcons.chevronRight),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('logout').tr(),
subtitle: Text('Log out of your account.'),
onTap: () {
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.logOut();
},
),
],
).padding(
horizontal: 8,
top: 8,
bottom: MediaQuery.of(context).padding.bottom,
),
),
);
}
}
class _UnauthorizedAccountScreen extends StatelessWidget {
const _UnauthorizedAccountScreen();
@override
Widget build(BuildContext context) {
return AppScaffold(
@ -14,15 +104,23 @@ class AccountScreen extends StatelessWidget {
body: Column(
children: <Widget>[
ListTile(
title: Text('Login'),
leading: const Icon(LucideIcons.userPlus),
trailing: const Icon(LucideIcons.chevronRight),
title: Text('createAccount').tr(),
subtitle: Text('New to here? We got you covered!'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
onTap: () {
context.router.push(LoginRoute());
context.router.push(CreateAccountRoute());
},
),
ListTile(
title: Text('Create an account'),
leading: const Icon(LucideIcons.logIn),
trailing: const Icon(LucideIcons.chevronRight),
subtitle: Text('Existing user? We\'re welcome you back!'),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('login').tr(),
onTap: () {
context.router.push(CreateAccountRoute());
context.router.push(LoginRoute());
},
),
],

View File

@ -9,7 +9,7 @@ import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -83,7 +83,7 @@ class CreateAccountScreen extends HookConsumerWidget {
alignment: Alignment.centerLeft,
child: CircleAvatar(
radius: 26,
child: Icon(MdiIcons.accountPlus, size: 28),
child: const Icon(LucideIcons.userPlus, size: 28),
).padding(bottom: 8),
),
Text(
@ -223,7 +223,10 @@ class CreateAccountScreen extends HookConsumerWidget {
children: [
Text('termAcceptLink').tr(),
const Gap(4),
Icon(MdiIcons.launch, size: 14),
const Icon(
LucideIcons.externalLink,
size: 14,
),
],
),
onTap: () {
@ -250,7 +253,7 @@ class CreateAccountScreen extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text("next").tr(),
Icon(MdiIcons.chevronRight),
const Icon(LucideIcons.chevronRight),
],
),
),

View File

@ -8,9 +8,10 @@ import 'package:gap/gap.dart';
import 'package:island/models/auth.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -128,7 +129,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
if (!context.mounted) return;
// TODO userinfo
final userNotifier = ref.read(userInfoProvider.notifier);
userNotifier.fetchUser();
Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
@ -145,7 +147,7 @@ class _LoginCheckScreen extends HookConsumerWidget {
alignment: Alignment.centerLeft,
child: CircleAvatar(
radius: 26,
child: Icon(MdiIcons.formTextboxPassword, size: 28),
child: const Icon(LucideIcons.squareAsterisk, size: 28),
).padding(bottom: 8),
),
Text(
@ -178,7 +180,10 @@ class _LoginCheckScreen extends HookConsumerWidget {
onPressed: isBusy.value ? null : () => performCheckTicket(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Text('next').tr(), Icon(MdiIcons.chevronRight)],
children: [
Text('next').tr(),
const Icon(LucideIcons.chevronRight),
],
),
),
],
@ -242,7 +247,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
alignment: Alignment.centerLeft,
child: CircleAvatar(
radius: 26,
child: Icon(MdiIcons.security, size: 28),
child: const Icon(LucideIcons.lock, size: 28),
).padding(bottom: 8),
),
Text(
@ -260,7 +265,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
secondary: Icon(MdiIcons.fileQuestion),
secondary: const Icon(LucideIcons.shieldQuestion),
title: Text('unknown').tr(),
enabled: !ticket!.blacklistFactors.contains(x.id),
value: factorPicked.value == x.id,
@ -288,7 +293,10 @@ class _LoginPickerScreen extends HookConsumerWidget {
onPressed: isBusy.value ? null : () => performGetFactorCode(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Text('next'.tr()), Icon(MdiIcons.chevronRight)],
children: [
Text('next'.tr()),
const Icon(LucideIcons.chevronRight),
],
),
),
],
@ -375,7 +383,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
alignment: Alignment.centerLeft,
child: CircleAvatar(
radius: 26,
child: Icon(MdiIcons.login, size: 28),
child: const Icon(LucideIcons.logIn, size: 28),
).padding(bottom: 8),
),
Text(
@ -409,7 +417,10 @@ class _LoginLookupScreen extends HookConsumerWidget {
onPressed: isBusy.value ? null : () => performNewTicket(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Text('next').tr(), Icon(MdiIcons.chevronRight)],
children: [
Text('next').tr(),
const Icon(LucideIcons.chevronRight),
],
),
),
],
@ -440,7 +451,7 @@ class _LoginLookupScreen extends HookConsumerWidget {
children: [
Text('termAcceptLink'.tr()),
const Gap(4),
Icon(MdiIcons.launch, size: 14),
const Icon(LucideIcons.externalLink, size: 14),
],
),
onTap: () {

View File

@ -4,9 +4,11 @@ import 'package:auto_route/auto_route.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.dart';
import 'package:island/route.gr.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -79,28 +81,54 @@ class WindowScaffold extends StatelessWidget {
),
);
}
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
body: SizedBox.expand(child: child),
key: rootScaffoldKey,
bottomNavigationBar: NavigationBar(
bottomNavigationBar:
router.current.meta['bottomNav'] == true || router.currentPath == '/'
? AppBottomNavigationBar(router: router)
: null,
);
}
}
class AppBottomNavigationBar extends HookConsumerWidget {
const AppBottomNavigationBar({super.key, required this.router});
final AppRouter router;
@override
Widget build(BuildContext context, WidgetRef ref) {
final destination = useState(0);
return NavigationBar(
selectedIndex: destination.value,
destinations: [
NavigationDestination(icon: Icon(MdiIcons.compass), label: 'Explore'),
NavigationDestination(icon: Icon(MdiIcons.account), label: 'Account'),
NavigationDestination(
icon: Icon(LucideIcons.compass),
label: 'Explore',
),
NavigationDestination(
icon: Icon(LucideIcons.userCircle),
label: 'Account',
),
],
onDestinationSelected: (idx) {
switch (idx) {
case 0:
destination.value = idx;
router.replace(ExploreRoute());
break;
case 1:
destination.value = idx;
router.replace(AccountRoute());
break;
}
},
),
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'image.dart';
import 'video.dart';
@ -35,7 +35,7 @@ class CloudFileWidget extends ConsumerWidget {
),
);
default:
return Placeholder();
return Text('Unable render for ${item.mimeType}');
}
}
}
@ -56,7 +56,7 @@ class ProfilePictureWidget extends ConsumerWidget {
color: Theme.of(context).colorScheme.primaryContainer,
child:
item == null
? Icon(MdiIcons.account)
? Icon(LucideIcons.userCircle)
: CloudFileWidget(item: item!),
),
);

View File

@ -733,6 +733,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: "direct main"
description:
name: lucide_icons
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
url: "https://pub.dev"
source: hosted
version: "0.257.0"
markdown:
dependency: "direct main"
description:
@ -757,14 +765,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.1"
material_design_icons_flutter:
dependency: "direct main"
description:
name: material_design_icons_flutter
sha256: "6f986b7a51f3ad4c00e33c5c84e8de1bdd140489bbcdc8b66fc1283dad4dea5a"
url: "https://pub.dev"
source: hosted
version: "7.0.7296"
media_kit:
dependency: "direct main"
description:

View File

@ -63,13 +63,13 @@ dependencies:
media_kit_libs_video: ^1.0.6
flutter_cache_manager: ^3.4.1
flutter_platform_alert: ^0.7.0
material_design_icons_flutter: ^7.0.7296
email_validator: ^3.0.0
easy_localization: ^3.0.7+1
flutter_inappwebview: ^6.1.5
animations: ^2.0.11
package_info_plus: ^8.3.0
device_info_plus: ^11.4.0
lucide_icons: ^0.257.0
dev_dependencies:
flutter_test: