From 0424eb0c2a129dacaf74a632f48a1c0be0e88035 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 12 Jun 2025 22:21:58 +0800 Subject: [PATCH] :sparkles: Enrich user profile settings --- assets/i18n/en-US.json | 12 +- ios/Podfile.lock | 6 + lib/main.dart | 9 + lib/models/user.dart | 8 + lib/models/user.freezed.dart | 108 +++++-- lib/models/user.g.dart | 30 ++ lib/screens/account/me/update.dart | 281 +++++++++++++++++- lib/services/time.dart | 18 ++ lib/services/timezone.dart | 1 + lib/services/timezone/native.dart | 22 ++ lib/services/timezone/web.dart | 21 ++ .../content/cloud_file_collection.dart | 11 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile | 2 +- macos/Runner.xcodeproj/project.pbxproj | 14 +- pubspec.lock | 32 +- pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 21 files changed, 546 insertions(+), 43 deletions(-) create mode 100644 lib/services/time.dart create mode 100644 lib/services/timezone.dart create mode 100644 lib/services/timezone/native.dart create mode 100644 lib/services/timezone/web.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 12f2eca3..105f3056 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -413,5 +413,15 @@ "chatBreakSet": "Break set for {}", "chatBreakCleared": "Chat break has been cleared.", "chatBreakCustom": "Custom duration", - "chatBreakEnterMinutes": "Enter minutes" + "chatBreakEnterMinutes": "Enter minutes", + "firstName": "First Name", + "middleName": "Middle Name", + "lastName": "Last Name", + "gender": "Gender", + "pronouns": "Pronouns", + "location": "Location", + "timeZone": "Time Zone", + "birthday": "Birthday", + "selectADate": "Select a date", + "useDeviceTimeZone": "Use device time zone" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 41764441..4933b9ce 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -84,6 +84,8 @@ PODS: - Flutter - flutter_platform_alert (0.0.1): - Flutter + - flutter_timezone (0.0.1): + - Flutter - flutter_udid (0.0.1): - Flutter - SAMKeychain @@ -204,6 +206,7 @@ DEPENDENCIES: - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - gal (from `.symlinks/plugins/gal/darwin`) @@ -268,6 +271,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_platform_alert: :path: ".symlinks/plugins/flutter_platform_alert/ios" + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" flutter_udid: :path: ".symlinks/plugins/flutter_udid/ios" flutter_webrtc: @@ -326,6 +331,7 @@ SPEC CHECKSUMS: flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 flutter_webrtc: fd0d3bdef8766a0736dbbe2e5b7e85f1f3c52117 gal: baecd024ebfd13c441269ca7404792a7152fde89 diff --git a/lib/main.dart b/lib/main.dart index f364c117..e2140489 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; import 'package:island/screens/auth/tabs.dart'; import 'package:island/services/notify.dart'; +import 'package:island/services/timezone.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:relative_time/relative_time.dart'; @@ -45,6 +46,14 @@ void main() async { showErrorAlert(err); } + try { + log("[SplashScreen] Loading timezone database..."); + await initializeTzdb(); + log("[SplashScreen] Time zone database was loaded!"); + } catch (err) { + log("[SplashScreen] Failed to load timezone database... $err"); + } + final prefs = await SharedPreferences.getInstance(); if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) { diff --git a/lib/models/user.dart b/lib/models/user.dart index 1dee290f..151272fc 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -31,6 +31,13 @@ sealed class SnAccountProfile with _$SnAccountProfile { required String? middleName, required String? lastName, @Default('') String bio, + @Default('') String gender, + @Default('') String pronouns, + @Default('') String location, + @Default('') String timeZone, + DateTime? birthday, + DateTime? lastSeenAt, + SnAccountBadge? activeBadge, required int experience, required int level, required double levelingProgress, @@ -79,6 +86,7 @@ sealed class SnAccountBadge with _$SnAccountBadge { required String accountId, required DateTime createdAt, required DateTime updatedAt, + required DateTime? activatedAt, required DateTime? deletedAt, }) = _SnAccountBadge; diff --git a/lib/models/user.freezed.dart b/lib/models/user.freezed.dart index ba297e49..9e045d84 100644 --- a/lib/models/user.freezed.dart +++ b/lib/models/user.freezed.dart @@ -200,7 +200,7 @@ $SnAccountProfileCopyWith<$Res> get profile { /// @nodoc mixin _$SnAccountProfile { - String get id; String? get firstName; String? get middleName; String? get lastName; String get bio; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String? get firstName; String? get middleName; String? get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; 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) @@ -213,16 +213,16 @@ $SnAccountProfileCopyWith get copyWith => _$SnAccountProfileCo @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.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + 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.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -233,11 +233,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> { factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; @useResult $Res call({ - String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? firstName, String? middleName, String? lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification; +$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification; } /// @nodoc @@ -250,14 +250,21 @@ class _$SnAccountProfileCopyWithImpl<$Res> /// 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 = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = 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 String,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: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable -as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable +as String,gender: null == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable +as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast_nullable_to_non_nullable +as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable +as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable +as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable +as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable +as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable @@ -273,6 +280,18 @@ as DateTime?, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$SnAccountBadgeCopyWith<$Res>? get activeBadge { + if (_self.activeBadge == null) { + return null; + } + + return $SnAccountBadgeCopyWith<$Res>(_self.activeBadge!, (value) { + return _then(_self.copyWith(activeBadge: 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 picture { if (_self.picture == null) { return null; @@ -313,7 +332,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification { @JsonSerializable() class _SnAccountProfile implements SnAccountProfile { - const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, this.bio = '', required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}); + const _SnAccountProfile({required this.id, required this.firstName, required this.middleName, required this.lastName, this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}); factory _SnAccountProfile.fromJson(Map json) => _$SnAccountProfileFromJson(json); @override final String id; @@ -321,6 +340,13 @@ class _SnAccountProfile implements SnAccountProfile { @override final String? middleName; @override final String? lastName; @override@JsonKey() final String bio; +@override@JsonKey() final String gender; +@override@JsonKey() final String pronouns; +@override@JsonKey() final String location; +@override@JsonKey() final String timeZone; +@override final DateTime? birthday; +@override final DateTime? lastSeenAt; +@override final SnAccountBadge? activeBadge; @override final int experience; @override final int level; @override final double levelingProgress; @@ -344,16 +370,16 @@ Map 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.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + 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.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -364,11 +390,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; @override @useResult $Res call({ - String id, String? firstName, String? middleName, String? lastName, String bio, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? firstName, String? middleName, String? lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); -@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification; +@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification; } /// @nodoc @@ -381,14 +407,21 @@ class __$SnAccountProfileCopyWithImpl<$Res> /// 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 = null,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = freezed,Object? middleName = freezed,Object? lastName = freezed,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = 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 String,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: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable -as String,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable +as String,gender: null == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable +as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast_nullable_to_non_nullable +as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable +as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable +as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable +as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable +as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable @@ -405,6 +438,18 @@ as DateTime?, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$SnAccountBadgeCopyWith<$Res>? get activeBadge { + if (_self.activeBadge == null) { + return null; + } + + return $SnAccountBadgeCopyWith<$Res>(_self.activeBadge!, (value) { + return _then(_self.copyWith(activeBadge: 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 picture { if (_self.picture == null) { return null; @@ -610,7 +655,7 @@ as DateTime?, /// @nodoc mixin _$SnAccountBadge { - String get id; String get type; String? get label; String? get caption; Map get meta; DateTime? get expiredAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String get type; String? get label; String? get caption; Map get meta; DateTime? get expiredAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get activatedAt; DateTime? get deletedAt; /// Create a copy of SnAccountBadge /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -623,16 +668,16 @@ $SnAccountBadgeCopyWith get copyWith => _$SnAccountBadgeCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.activatedAt, activatedAt) || other.activatedAt == activatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(meta),expiredAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(meta),expiredAt,accountId,createdAt,updatedAt,activatedAt,deletedAt); @override String toString() { - return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, activatedAt: $activatedAt, deletedAt: $deletedAt)'; } @@ -643,7 +688,7 @@ abstract mixin class $SnAccountBadgeCopyWith<$Res> { factory $SnAccountBadgeCopyWith(SnAccountBadge value, $Res Function(SnAccountBadge) _then) = _$SnAccountBadgeCopyWithImpl; @useResult $Res call({ - String id, String type, String? label, String? caption, Map meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String type, String? label, String? caption, Map meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? activatedAt, DateTime? deletedAt }); @@ -660,7 +705,7 @@ class _$SnAccountBadgeCopyWithImpl<$Res> /// Create a copy of SnAccountBadge /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? activatedAt = freezed,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -671,7 +716,8 @@ as Map,expiredAt: freezed == expiredAt ? _self.expiredAt : expi as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String,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,activatedAt: freezed == activatedAt ? _self.activatedAt : activatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?, )); } @@ -683,7 +729,7 @@ as DateTime?, @JsonSerializable() class _SnAccountBadge implements SnAccountBadge { - const _SnAccountBadge({required this.id, required this.type, required this.label, required this.caption, required final Map meta, required this.expiredAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta; + const _SnAccountBadge({required this.id, required this.type, required this.label, required this.caption, required final Map meta, required this.expiredAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.activatedAt, required this.deletedAt}): _meta = meta; factory _SnAccountBadge.fromJson(Map json) => _$SnAccountBadgeFromJson(json); @override final String id; @@ -701,6 +747,7 @@ class _SnAccountBadge implements SnAccountBadge { @override final String accountId; @override final DateTime createdAt; @override final DateTime updatedAt; +@override final DateTime? activatedAt; @override final DateTime? deletedAt; /// Create a copy of SnAccountBadge @@ -716,16 +763,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountBadge&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.label, label) || other.label == label)&&(identical(other.caption, caption) || other.caption == caption)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.activatedAt, activatedAt) || other.activatedAt == activatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(_meta),expiredAt,accountId,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,type,label,caption,const DeepCollectionEquality().hash(_meta),expiredAt,accountId,createdAt,updatedAt,activatedAt,deletedAt); @override String toString() { - return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccountBadge(id: $id, type: $type, label: $label, caption: $caption, meta: $meta, expiredAt: $expiredAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, activatedAt: $activatedAt, deletedAt: $deletedAt)'; } @@ -736,7 +783,7 @@ abstract mixin class _$SnAccountBadgeCopyWith<$Res> implements $SnAccountBadgeCo factory _$SnAccountBadgeCopyWith(_SnAccountBadge value, $Res Function(_SnAccountBadge) _then) = __$SnAccountBadgeCopyWithImpl; @override @useResult $Res call({ - String id, String type, String? label, String? caption, Map meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String type, String? label, String? caption, Map meta, DateTime? expiredAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? activatedAt, DateTime? deletedAt }); @@ -753,7 +800,7 @@ class __$SnAccountBadgeCopyWithImpl<$Res> /// Create a copy of SnAccountBadge /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? label = freezed,Object? caption = freezed,Object? meta = null,Object? expiredAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? activatedAt = freezed,Object? deletedAt = freezed,}) { return _then(_SnAccountBadge( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -764,7 +811,8 @@ as Map,expiredAt: freezed == expiredAt ? _self.expiredAt : expi as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as String,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,activatedAt: freezed == activatedAt ? _self.activatedAt : activatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?, )); } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 20d01968..a3ae041a 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -47,6 +47,24 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map json) => middleName: json['middle_name'] as String?, lastName: json['last_name'] as String?, bio: json['bio'] as String? ?? '', + gender: json['gender'] as String? ?? '', + pronouns: json['pronouns'] as String? ?? '', + location: json['location'] as String? ?? '', + timeZone: json['time_zone'] as String? ?? '', + birthday: + json['birthday'] == null + ? null + : DateTime.parse(json['birthday'] as String), + lastSeenAt: + json['last_seen_at'] == null + ? null + : DateTime.parse(json['last_seen_at'] as String), + activeBadge: + json['active_badge'] == null + ? null + : SnAccountBadge.fromJson( + json['active_badge'] as Map, + ), experience: (json['experience'] as num).toInt(), level: (json['level'] as num).toInt(), levelingProgress: (json['leveling_progress'] as num).toDouble(), @@ -81,6 +99,13 @@ Map _$SnAccountProfileToJson(_SnAccountProfile instance) => 'middle_name': instance.middleName, 'last_name': instance.lastName, 'bio': instance.bio, + 'gender': instance.gender, + 'pronouns': instance.pronouns, + 'location': instance.location, + 'time_zone': instance.timeZone, + 'birthday': instance.birthday?.toIso8601String(), + 'last_seen_at': instance.lastSeenAt?.toIso8601String(), + 'active_badge': instance.activeBadge?.toJson(), 'experience': instance.experience, 'level': instance.level, 'leveling_progress': instance.levelingProgress, @@ -144,6 +169,10 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map json) => accountId: json['account_id'] as String, createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), + activatedAt: + json['activated_at'] == null + ? null + : DateTime.parse(json['activated_at'] as String), deletedAt: json['deleted_at'] == null ? null @@ -161,6 +190,7 @@ Map _$SnAccountBadgeToJson(_SnAccountBadge instance) => 'account_id': instance.accountId, 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), + 'activated_at': instance.activatedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; diff --git a/lib/screens/account/me/update.dart b/lib/screens/account/me/update.dart index d7c268b0..655befdb 100644 --- a/lib/screens/account/me/update.dart +++ b/lib/screens/account/me/update.dart @@ -11,6 +11,7 @@ import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/file.dart'; +import 'package:island/services/timezone.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -120,9 +121,31 @@ class UpdateProfileScreen extends HookConsumerWidget { } final formKeyProfile = useMemoized(GlobalKey.new, const []); + final birthday = useState(user.value!.profile.birthday); + final firstNameController = useTextEditingController( + text: user.value!.profile.firstName, + ); + final middleNameController = useTextEditingController( + text: user.value!.profile.middleName, + ); + final lastNameController = useTextEditingController( + text: user.value!.profile.lastName, + ); final bioController = useTextEditingController( text: user.value!.profile.bio, ); + final genderController = useTextEditingController( + text: user.value!.profile.gender, + ); + final pronounsController = useTextEditingController( + text: user.value!.profile.pronouns, + ); + final locationController = useTextEditingController( + text: user.value!.profile.location, + ); + final timeZoneController = useTextEditingController( + text: user.value!.profile.timeZone, + ); void updateProfile() async { if (!formKeyProfile.currentState!.validate()) return; @@ -132,7 +155,17 @@ class UpdateProfileScreen extends HookConsumerWidget { final client = ref.watch(apiClientProvider); await client.patch( '/accounts/me/profile', - data: {'bio': bioController.text}, + data: { + 'bio': bioController.text, + 'first_name': firstNameController.text, + 'middle_name': middleNameController.text, + 'last_name': lastNameController.text, + 'gender': genderController.text, + 'pronouns': pronounsController.text, + 'location': locationController.text, + 'time_zone': timeZoneController.text, + 'birthday': birthday.value?.toIso8601String(), + }, ); final userNotifier = ref.read(userInfoProvider.notifier); userNotifier.fetchUser(); @@ -268,6 +301,45 @@ class UpdateProfileScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 16, children: [ + Row( + spacing: 16, + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'firstName'.tr(), + ), + controller: firstNameController, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'middleName'.tr(), + ), + controller: middleNameController, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'lastName'.tr(), + ), + controller: lastNameController, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ], + ), + TextFormField( decoration: InputDecoration(labelText: 'bio'.tr()), maxLines: null, @@ -276,6 +348,213 @@ class UpdateProfileScreen extends HookConsumerWidget { onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), + Row( + spacing: 16, + children: [ + Expanded( + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + final options = ['Male', 'Female']; + if (textEditingValue.text == '') { + return options; + } + return options.where( + (option) => option.toLowerCase().contains( + textEditingValue.text.toLowerCase(), + ), + ); + }, + onSelected: (String selection) { + genderController.text = selection; + }, + fieldViewBuilder: ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Initialize the controller with the current value + if (controller.text.isEmpty && + genderController.text.isNotEmpty) { + controller.text = genderController.text; + } + + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'gender'.tr(), + ), + onChanged: (value) { + genderController.text = value; + }, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus + ?.unfocus(), + ); + }, + ), + ), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'pronouns'.tr(), + ), + controller: pronounsController, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ], + ), + Row( + spacing: 16, + children: [ + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: 'location'.tr(), + ), + controller: locationController, + onTapOutside: + (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + Expanded( + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + final lowercaseQuery = + textEditingValue.text.toLowerCase(); + return getAvailableTz().where((tz) { + return tz.toLowerCase().contains(lowercaseQuery); + }); + }, + onSelected: (String selection) { + timeZoneController.text = selection; + }, + fieldViewBuilder: ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + // Sync the controller with timeZoneController when the widget is built + if (controller.text != timeZoneController.text) { + controller.text = timeZoneController.text; + } + + return TextFormField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'timeZone'.tr(), + suffix: InkWell( + child: const Icon( + Symbols.my_location, + size: 18, + ), + onTap: () async { + try { + showLoadingModal(context); + final machineTz = await getMachineTz(); + controller.text = machineTz; + timeZoneController.text = machineTz; + } finally { + if (context.mounted) { + hideLoadingModal(context); + } + } + }, + ), + ), + onChanged: (value) { + timeZoneController.text = value; + }, + ); + }, + optionsViewBuilder: (context, onSelected, options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200, + maxWidth: 300, + ), + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemCount: options.length, + itemBuilder: ( + BuildContext context, + int index, + ) { + final option = options.elementAt(index); + return ListTile( + title: Text( + option, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + onSelected(option); + }, + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ), + GestureDetector( + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: birthday.value ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime.now(), + ); + if (date != null) { + birthday.value = date; + } + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'birthday'.tr(), + style: TextStyle( + color: Theme.of(context).hintColor, + ), + ), + Text( + birthday.value != null + ? DateFormat.yMMMd().format(birthday.value!) + : 'Select a date'.tr(), + ), + ], + ), + ), + ), Align( alignment: Alignment.centerRight, child: TextButton.icon( diff --git a/lib/services/time.dart b/lib/services/time.dart new file mode 100644 index 00000000..31bd462e --- /dev/null +++ b/lib/services/time.dart @@ -0,0 +1,18 @@ +extension DurationFormatter on Duration { + String formatDuration() { + final isNegative = inMicroseconds < 0; + final positiveDuration = isNegative ? -this : this; + + final hours = positiveDuration.inHours.toString().padLeft(2, '0'); + final minutes = (positiveDuration.inMinutes % 60).toString().padLeft( + 2, + '0', + ); + final seconds = (positiveDuration.inSeconds % 60).toString().padLeft( + 2, + '0', + ); + + return '${isNegative ? '-' : ''}$hours:$minutes:$seconds'; + } +} diff --git a/lib/services/timezone.dart b/lib/services/timezone.dart new file mode 100644 index 00000000..c532e092 --- /dev/null +++ b/lib/services/timezone.dart @@ -0,0 +1 @@ +export 'timezone/native.dart' if (dart.library.html) 'timezone/web.dart'; diff --git a/lib/services/timezone/native.dart b/lib/services/timezone/native.dart new file mode 100644 index 00000000..573916c6 --- /dev/null +++ b/lib/services/timezone/native.dart @@ -0,0 +1,22 @@ +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:timezone/standalone.dart' as tz; +import 'package:timezone/data/latest_all.dart' as tzdb; + +Future initializeTzdb() async { + tzdb.initializeTimeZones(); +} + +(Duration offset, DateTime now) getTzInfo(String name) { + final location = tz.getLocation(name); + final now = tz.TZDateTime.now(location); + final offset = now.timeZoneOffset; + return (offset, now); +} + +Future getMachineTz() async { + return await FlutterTimezone.getLocalTimezone(); +} + +List getAvailableTz() { + return tz.timeZoneDatabase.locations.keys.toList(); +} diff --git a/lib/services/timezone/web.dart b/lib/services/timezone/web.dart new file mode 100644 index 00000000..c1ebbc87 --- /dev/null +++ b/lib/services/timezone/web.dart @@ -0,0 +1,21 @@ +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:timezone/browser.dart' as tz; + +Future initializeTzdb() async { + await tz.initializeTimeZone(); +} + +(Duration offset, DateTime now) getTzInfo(String name) { + final location = tz.getLocation(name); + final now = tz.TZDateTime.now(location); + final offset = now.timeZoneOffset; + return (offset, now); +} + +Future getMachineTz() async { + return await FlutterTimezone.getLocalTimezone(); +} + +List getAvailableTz() { + return tz.timeZoneDatabase.locations.keys.toList(); +} diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index 0501296b..0e17ea3b 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -10,6 +10,7 @@ import 'package:gal/gal.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; +import 'package:island/pods/network.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:path/path.dart' show extension; import 'package:path_provider/path_provider.dart'; @@ -194,14 +195,18 @@ class CloudFileZoomIn extends HookConsumerWidget { ); // Get the image URL - final imageUrl = '$serverUrl/files/${item.id}?original=true'; + final client = ref.watch(apiClientProvider); // Create a temporary file to save the image final tempDir = await getTemporaryDirectory(); final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}'; - await Dio().download(imageUrl, filePath); - await Gal.putImage(filePath); + await client.download( + '/files/${item.id}', + filePath, + queryParameters: {'original': true}, + ); + await Gal.putImage(filePath, album: 'Solar Network'); // Show success message scaffold.showSnackBar( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 43372a22..f56efee4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin"); flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar); + g_autoptr(FlPluginRegistrar) flutter_timezone_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin"); + flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar); g_autoptr(FlPluginRegistrar) flutter_udid_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin"); flutter_udid_plugin_register_with_registrar(flutter_udid_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9bf6e185..6f569e8d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux file_selector_linux flutter_platform_alert + flutter_timezone flutter_udid flutter_webrtc irondash_engine_context diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 36061b63..cf37ccd8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import firebase_core import firebase_messaging import flutter_inappwebview_macos import flutter_platform_alert +import flutter_timezone import flutter_udid import flutter_webrtc import gal @@ -43,6 +44,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) + FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/macos/Podfile b/macos/Podfile index ff5ddb3b..a46f7f23 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.15' +platform :osx, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f3ee99a2..d30fc74e 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -384,10 +384,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -423,10 +427,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -593,7 +601,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -730,7 +738,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -755,7 +763,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/pubspec.lock b/pubspec.lock index 499adbf6..363ed127 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -589,10 +589,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" + sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a url: "https://pub.dev" source: hosted - version: "10.1.9" + version: "10.2.0" file_selector_linux: dependency: transitive description: @@ -867,6 +867,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_popup_card: + dependency: "direct main" + description: + name: flutter_popup_card + sha256: "71bca1521e3bae790162183d4dbfb77b84e11a9604e4b5f3b0edad6c5a1495cd" + url: "https://pub.dev" + source: hosted + version: "0.0.6" flutter_riverpod: dependency: "direct main" description: @@ -888,6 +896,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934" + url: "https://pub.dev" + source: hosted + version: "4.1.1" flutter_udid: dependency: "direct main" description: @@ -2138,6 +2154,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.8" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" timing: dependency: transitive description: @@ -2351,10 +2375,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d7b17f13..503bd06c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,9 @@ dependencies: qr_flutter: ^4.1.0 flutter_otp_text_field: ^1.5.1+1 palette_generator: ^0.3.3+7 + flutter_popup_card: ^0.0.6 + timezone: ^0.10.1 + flutter_timezone: ^4.1.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bb3a149e..0ae8b189 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterPlatformAlertPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin")); + FlutterTimezonePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); FlutterUdidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); FlutterWebRTCPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 91a19779..1c7bca6e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core flutter_inappwebview_windows flutter_platform_alert + flutter_timezone flutter_udid flutter_webrtc gal