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 12f2eca..105f305 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 4176444..4933b9c 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 f364c11..e214048 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 1dee290..151272f 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 ba297e4..9e045d8 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 20d0196..a3ae041 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 d7c268b..655befd 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 0000000..31bd462 --- /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 0000000..c532e09 --- /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 0000000..573916c --- /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 0000000..c1ebbc8 --- /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 0501296..0e17ea3 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 43372a2..f56efee 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 9bf6e18..6f569e8 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 36061b6..cf37ccd 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 ff5ddb3..a46f7f2 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 f3ee99a..d30fc74 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 499adbf..363ed12 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 d7b17f1..503bd06 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 bb3a149..0ae8b18 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 91a1977..1c7bca6 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