Compare commits

...

34 Commits

Author SHA1 Message Date
5efa9b2ae8 🚀 Launch 3.3.0+135 2025-10-13 01:25:37 +08:00
dd3e39e891 💄 Smart retry GET requests 2025-10-13 00:58:51 +08:00
b6896ded23 🐛 Fix compose settings bugs 2025-10-13 00:55:45 +08:00
f28a73ff9c ♻️ Refactor post tags 2025-10-13 00:53:45 +08:00
a014b64235 🐛 Fix performance issue and recycle logic issue 2025-10-13 00:36:40 +08:00
7e0e7c20d7 ♻️ Convert the activity calandar screen to sheet 2025-10-13 00:34:16 +08:00
389fa515ba Able to turn off animation (for message only for now) 2025-10-13 00:22:30 +08:00
681ead02eb 🐛 Fix markdown rendering 2025-10-13 00:16:57 +08:00
8d1c145b0b 💫 Message list fully animated 2025-10-13 00:16:50 +08:00
51b4754182 💄 Optimize chat room, chat input
💫 More animations in chat input
2025-10-13 00:05:22 +08:00
8a2b321701 Custom color name 2025-10-12 23:53:28 +08:00
f685a7a249 🐛 Bug fixes in developer hub 2025-10-12 23:27:49 +08:00
76009147e9 🐛 Fix message notifier 2025-10-12 23:23:13 +08:00
ce12f28e56 Custom reaction via sticker 2025-10-12 23:18:58 +08:00
3604373a1e 🐛 Fix update message 2025-10-12 22:43:25 +08:00
9704a4c2c7 ♻️ Use the dialog when launch from quick reply to post 2025-10-12 22:32:43 +08:00
67def56ad1 💄 Better post quick reply 2025-10-12 22:26:37 +08:00
1be33916af 💄 Optimize post reaction sheet 2025-10-12 22:21:15 +08:00
e8ff1bfd22 🐛 Bug fixes and optimization 2025-10-12 22:08:33 +08:00
3ae56f3d89 Post reaction detail popover 2025-10-12 22:05:47 +08:00
707143e998 ♻️ Extract post reaction sheet from post item 2025-10-12 21:38:24 +08:00
1fd34eb2a3 💄 Merge the creator hub and developer hub to the tabs 2025-10-12 21:32:34 +08:00
d7ca41e946 ♻️ Refactored heatmap 2025-10-12 20:58:41 +08:00
ad9fb0719a 🐛 Bug fixes and optimization 2025-10-12 20:45:10 +08:00
e2d315afd4 Auto complete in post as well 2025-10-12 19:49:16 +08:00
6124dbfd79 Heatmap 2025-10-12 18:47:01 +08:00
5327f04ec0 ♻️ Gloablize pop intent 2025-10-12 18:26:28 +08:00
41c56a2319 Markdown++ 2025-10-12 18:22:16 +08:00
f9d033542e Mention tag in markdown 2025-10-12 17:56:31 +08:00
91784e65e6 Message item swipe to 2025-10-12 17:29:03 +08:00
9d39c6a825 Auto complete, better metion parser, sticker placeholder v2 2025-10-12 17:10:18 +08:00
537e49f1a4 Add typing ahead 2025-10-12 16:15:04 +08:00
75bbd4df71 Desktop page actions on title bar 2025-10-12 16:14:54 +08:00
6ef4580d93 💄 Optimize developer hub 2025-10-12 16:14:44 +08:00
66 changed files with 6143 additions and 1513 deletions

View File

@@ -25,13 +25,8 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() }
defaultConfig {
applicationId = "dev.solsynth.solian"
// You can update the following values to match your application needs.
@@ -75,3 +70,17 @@ dependencies {
flutter {
source = "../.."
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
jvmToolchain(21)
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

View File

@@ -135,6 +135,11 @@
"reactionPositive": "Postive",
"reactionNegative": "Negative",
"reactionNeutral": "Neutral",
"customReaction": "Custom Reaction",
"customReactions": "Custom Reactions",
"stickerPlaceholder": "Sticker Placeholder",
"reactionAttitude": "Reaction Attitude",
"addReaction": "Add Reaction",
"connectionConnected": "Connected",
"connectionDisconnected": "Disconnected",
"connectionReconnecting": "Reconnecting",
@@ -166,8 +171,8 @@
"checkInResultLevel3": "Good Luck",
"checkInResultLevel4": "Best Luck",
"checkInActivityTitle": "{} checked in on {} and got a {}",
"eventCalander": "Event Calander",
"eventCalanderEmpty": "No events on that day.",
"eventCalendar": "Event Calendar",
"eventCalendarEmpty": "No events on that day.",
"fortuneGraph": "Fortune Trend",
"noFortuneData": "No fortune data available for this month.",
"creatorHub": "Creator Hub",
@@ -474,7 +479,6 @@
"accountProfileView": "View Profile",
"unspecified": "Unspecified",
"added": "Added",
"preview": "Preview",
"togglePreview": "Toggle Preview",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
@@ -635,6 +639,10 @@
"chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat",
"chatReplyingTo": "Replying to {}",
"chatForwarding": "Forwarding message",
"chatEditing": "Editing message",
"chatNoContent": "No content",
"realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.",
"search": "Search",
@@ -1215,5 +1223,34 @@
"resend": "Resend",
"fileInfoTitle": "File Information",
"download": "Download",
"info": "Info"
"info": "Info",
"noStickers": "No Stickers",
"noStickersInPack": "This pack does not contains stickers",
"noStickerPacks": "No Sticker Packs",
"refresh": "Refresh",
"spoiler": "Spoiler",
"activityHeatmap": "Activity Heatmap",
"custom": "Custom",
"usernameColor": "Username Color",
"colorType": "Color Type",
"plain": "Plain",
"gradient": "Gradient",
"colorValue": "Color Value",
"gradientDirection": "Gradient Direction",
"gradientDirectionToRight": "To Right",
"gradientDirectionToLeft": "To Left",
"gradientDirectionToBottom": "To Bottom",
"gradientDirectionToTop": "To Top",
"gradientDirectionToBottomRight": "To Bottom Right",
"gradientDirectionToBottomLeft": "To Bottom Left",
"gradientDirectionToTopRight": "To Top Right",
"gradientDirectionToTopLeft": "To Top Left",
"gradientColors": "Gradient Colors",
"color": "Color",
"addColor": "Add Color",
"preview": "Preview",
"availableWithYourPlan": "Available with your plan",
"upgradeRequired": "Upgrade required",
"settingsDisableAnimation": "Disable Animation",
"addTag": "Add Tag"
}

View File

@@ -940,7 +940,7 @@
"editBot": "编辑机器人",
"botAutomatedBy": "由 {} 自动化",
"botDetails": "机器人详情",
"overview": "总",
"overview": "总",
"keys": "密钥",
"botNotFound": "机器人未找到。",
"newBotKey": "新建密钥",

View File

@@ -55,6 +55,19 @@ class ProfileLinkConverter
}
}
@freezed
sealed class UsernameColor with _$UsernameColor {
const factory UsernameColor({
@Default('plain') String type,
String? value,
String? direction,
List<String>? colors,
}) = _UsernameColor;
factory UsernameColor.fromJson(Map<String, dynamic> json) =>
_$UsernameColorFromJson(json);
}
@freezed
sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
@@ -79,6 +92,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
required SnCloudFile? picture,
required SnCloudFile? background,
required SnVerificationMark? verification,
UsernameColor? usernameColor,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,

View File

@@ -622,10 +622,284 @@ as String,
}
/// @nodoc
mixin _$UsernameColor {
String get type; String? get value; String? get direction; List<String>? get colors;
/// Create a copy of UsernameColor
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UsernameColorCopyWith<UsernameColor> get copyWith => _$UsernameColorCopyWithImpl<UsernameColor>(this as UsernameColor, _$identity);
/// Serializes this UsernameColor to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.colors, colors));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(colors));
@override
String toString() {
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
}
}
/// @nodoc
abstract mixin class $UsernameColorCopyWith<$Res> {
factory $UsernameColorCopyWith(UsernameColor value, $Res Function(UsernameColor) _then) = _$UsernameColorCopyWithImpl;
@useResult
$Res call({
String type, String? value, String? direction, List<String>? colors
});
}
/// @nodoc
class _$UsernameColorCopyWithImpl<$Res>
implements $UsernameColorCopyWith<$Res> {
_$UsernameColorCopyWithImpl(this._self, this._then);
final UsernameColor _self;
final $Res Function(UsernameColor) _then;
/// Create a copy of UsernameColor
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
as String?,colors: freezed == colors ? _self.colors : colors // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// Adds pattern-matching-related methods to [UsernameColor].
extension UsernameColorPatterns on UsernameColor {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UsernameColor value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UsernameColor() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UsernameColor value) $default,){
final _that = this;
switch (_that) {
case _UsernameColor():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UsernameColor value)? $default,){
final _that = this;
switch (_that) {
case _UsernameColor() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String? value, String? direction, List<String>? colors)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UsernameColor() when $default != null:
return $default(_that.type,_that.value,_that.direction,_that.colors);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String? value, String? direction, List<String>? colors) $default,) {final _that = this;
switch (_that) {
case _UsernameColor():
return $default(_that.type,_that.value,_that.direction,_that.colors);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String? value, String? direction, List<String>? colors)? $default,) {final _that = this;
switch (_that) {
case _UsernameColor() when $default != null:
return $default(_that.type,_that.value,_that.direction,_that.colors);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _UsernameColor implements UsernameColor {
const _UsernameColor({this.type = 'plain', this.value, this.direction, final List<String>? colors}): _colors = colors;
factory _UsernameColor.fromJson(Map<String, dynamic> json) => _$UsernameColorFromJson(json);
@override@JsonKey() final String type;
@override final String? value;
@override final String? direction;
final List<String>? _colors;
@override List<String>? get colors {
final value = _colors;
if (value == null) return null;
if (_colors is EqualUnmodifiableListView) return _colors;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of UsernameColor
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UsernameColorCopyWith<_UsernameColor> get copyWith => __$UsernameColorCopyWithImpl<_UsernameColor>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UsernameColorToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._colors, _colors));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(_colors));
@override
String toString() {
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
}
}
/// @nodoc
abstract mixin class _$UsernameColorCopyWith<$Res> implements $UsernameColorCopyWith<$Res> {
factory _$UsernameColorCopyWith(_UsernameColor value, $Res Function(_UsernameColor) _then) = __$UsernameColorCopyWithImpl;
@override @useResult
$Res call({
String type, String? value, String? direction, List<String>? colors
});
}
/// @nodoc
class __$UsernameColorCopyWithImpl<$Res>
implements _$UsernameColorCopyWith<$Res> {
__$UsernameColorCopyWithImpl(this._self, this._then);
final _UsernameColor _self;
final $Res Function(_UsernameColor) _then;
/// Create a copy of UsernameColor
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
return _then(_UsernameColor(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
as String?,colors: freezed == colors ? _self._colors : colors // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// @nodoc
mixin _$SnAccountProfile {
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;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; 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;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; UsernameColor? get usernameColor; 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)
@@ -638,16 +912,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> 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.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)&&const DeepCollectionEquality().equals(other.links, links)&&(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.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(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)&&const DeepCollectionEquality().equals(other.links, links)&&(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.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(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.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, 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, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -658,11 +932,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, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, 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,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$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;$UsernameColorCopyWith<$Res>? get usernameColor;
}
/// @nodoc
@@ -675,7 +949,7 @@ 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 = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = 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 = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = 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: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -698,7 +972,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
as UsernameColor?,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?,
@@ -752,6 +1027,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UsernameColorCopyWith<$Res>? get usernameColor {
if (_self.usernameColor == null) {
return null;
}
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
return _then(_self.copyWith(usernameColor: value));
});
}
}
@@ -831,10 +1118,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -852,10 +1139,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -869,10 +1156,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -884,7 +1171,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
@JsonSerializable()
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, this.usernameColor, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id;
@@ -914,6 +1201,7 @@ class _SnAccountProfile implements SnAccountProfile {
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnVerificationMark? verification;
@override final UsernameColor? usernameColor;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@@ -931,16 +1219,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.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)&&const DeepCollectionEquality().equals(other._links, _links)&&(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.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(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)&&const DeepCollectionEquality().equals(other._links, _links)&&(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.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(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.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
@override
String toString() {
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, 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, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -951,11 +1239,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, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, 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,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@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;@override $UsernameColorCopyWith<$Res>? get usernameColor;
}
/// @nodoc
@@ -968,7 +1256,7 @@ 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 = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = 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 = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = 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: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
@@ -991,7 +1279,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
as UsernameColor?,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?,
@@ -1046,6 +1335,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UsernameColorCopyWith<$Res>? get usernameColor {
if (_self.usernameColor == null) {
return null;
}
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
return _then(_self.copyWith(usernameColor: value));
});
}
}

View File

@@ -63,6 +63,23 @@ _ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
<String, dynamic>{'name': instance.name, 'url': instance.url};
_UsernameColor _$UsernameColorFromJson(Map<String, dynamic> json) =>
_UsernameColor(
type: json['type'] as String? ?? 'plain',
value: json['value'] as String?,
direction: json['direction'] as String?,
colors:
(json['colors'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
<String, dynamic>{
'type': instance.type,
'value': instance.value,
'direction': instance.direction,
'colors': instance.colors,
};
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: json['id'] as String,
@@ -113,6 +130,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
usernameColor:
json['username_color'] == null
? null
: UsernameColor.fromJson(
json['username_color'] as Map<String, dynamic>,
),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
@@ -144,6 +167,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'username_color': instance.usernameColor?.toJson(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'autocomplete_response.freezed.dart';
part 'autocomplete_response.g.dart';
@freezed
sealed class AutocompleteSuggestion with _$AutocompleteSuggestion {
const factory AutocompleteSuggestion({
required String type,
required String keyword,
required dynamic data,
}) = _AutocompleteSuggestion;
factory AutocompleteSuggestion.fromJson(Map<String, dynamic> json) =>
_$AutocompleteSuggestionFromJson(json);
}

View File

@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'autocomplete_response.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AutocompleteSuggestion {
String get type; String get keyword; dynamic get data;
/// Create a copy of AutocompleteSuggestion
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AutocompleteSuggestionCopyWith<AutocompleteSuggestion> get copyWith => _$AutocompleteSuggestionCopyWithImpl<AutocompleteSuggestion>(this as AutocompleteSuggestion, _$identity);
/// Serializes this AutocompleteSuggestion to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
@override
String toString() {
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
}
}
/// @nodoc
abstract mixin class $AutocompleteSuggestionCopyWith<$Res> {
factory $AutocompleteSuggestionCopyWith(AutocompleteSuggestion value, $Res Function(AutocompleteSuggestion) _then) = _$AutocompleteSuggestionCopyWithImpl;
@useResult
$Res call({
String type, String keyword, dynamic data
});
}
/// @nodoc
class _$AutocompleteSuggestionCopyWithImpl<$Res>
implements $AutocompleteSuggestionCopyWith<$Res> {
_$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
final AutocompleteSuggestion _self;
final $Res Function(AutocompleteSuggestion) _then;
/// Create a copy of AutocompleteSuggestion
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,
));
}
}
/// Adds pattern-matching-related methods to [AutocompleteSuggestion].
extension AutocompleteSuggestionPatterns on AutocompleteSuggestion {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AutocompleteSuggestion() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value) $default,){
final _that = this;
switch (_that) {
case _AutocompleteSuggestion():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AutocompleteSuggestion value)? $default,){
final _that = this;
switch (_that) {
case _AutocompleteSuggestion() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AutocompleteSuggestion() when $default != null:
return $default(_that.type,_that.keyword,_that.data);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data) $default,) {final _that = this;
switch (_that) {
case _AutocompleteSuggestion():
return $default(_that.type,_that.keyword,_that.data);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String keyword, dynamic data)? $default,) {final _that = this;
switch (_that) {
case _AutocompleteSuggestion() when $default != null:
return $default(_that.type,_that.keyword,_that.data);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _AutocompleteSuggestion implements AutocompleteSuggestion {
const _AutocompleteSuggestion({required this.type, required this.keyword, required this.data});
factory _AutocompleteSuggestion.fromJson(Map<String, dynamic> json) => _$AutocompleteSuggestionFromJson(json);
@override final String type;
@override final String keyword;
@override final dynamic data;
/// Create a copy of AutocompleteSuggestion
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AutocompleteSuggestionCopyWith<_AutocompleteSuggestion> get copyWith => __$AutocompleteSuggestionCopyWithImpl<_AutocompleteSuggestion>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AutocompleteSuggestionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
@override
String toString() {
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
}
}
/// @nodoc
abstract mixin class _$AutocompleteSuggestionCopyWith<$Res> implements $AutocompleteSuggestionCopyWith<$Res> {
factory _$AutocompleteSuggestionCopyWith(_AutocompleteSuggestion value, $Res Function(_AutocompleteSuggestion) _then) = __$AutocompleteSuggestionCopyWithImpl;
@override @useResult
$Res call({
String type, String keyword, dynamic data
});
}
/// @nodoc
class __$AutocompleteSuggestionCopyWithImpl<$Res>
implements _$AutocompleteSuggestionCopyWith<$Res> {
__$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
final _AutocompleteSuggestion _self;
final $Res Function(_AutocompleteSuggestion) _then;
/// Create a copy of AutocompleteSuggestion
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
return _then(_AutocompleteSuggestion(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as dynamic,
));
}
}
// dart format on

View File

@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'autocomplete_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_AutocompleteSuggestion _$AutocompleteSuggestionFromJson(
Map<String, dynamic> json,
) => _AutocompleteSuggestion(
type: json['type'] as String,
keyword: json['keyword'] as String,
data: json['data'],
);
Map<String, dynamic> _$AutocompleteSuggestionToJson(
_AutocompleteSuggestion instance,
) => <String, dynamic>{
'type': instance.type,
'keyword': instance.keyword,
'data': instance.data,
};

26
lib/models/heatmap.dart Normal file
View File

@@ -0,0 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'heatmap.freezed.dart';
part 'heatmap.g.dart';
@freezed
sealed class SnHeatmap with _$SnHeatmap {
const factory SnHeatmap({
required String unit,
@JsonKey(name: 'period_start') required DateTime periodStart,
@JsonKey(name: 'period_end') required DateTime periodEnd,
required List<SnHeatmapItem> items,
}) = _SnHeatmap;
factory SnHeatmap.fromJson(Map<String, dynamic> json) =>
_$SnHeatmapFromJson(json);
}
@freezed
sealed class SnHeatmapItem with _$SnHeatmapItem {
const factory SnHeatmapItem({required DateTime date, required int count}) =
_SnHeatmapItem;
factory SnHeatmapItem.fromJson(Map<String, dynamic> json) =>
_$SnHeatmapItemFromJson(json);
}

View File

@@ -0,0 +1,546 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'heatmap.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnHeatmap {
String get unit;@JsonKey(name: 'period_start') DateTime get periodStart;@JsonKey(name: 'period_end') DateTime get periodEnd; List<SnHeatmapItem> get items;
/// Create a copy of SnHeatmap
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnHeatmapCopyWith<SnHeatmap> get copyWith => _$SnHeatmapCopyWithImpl<SnHeatmap>(this as SnHeatmap, _$identity);
/// Serializes this SnHeatmap to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other.items, items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(items));
@override
String toString() {
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
}
}
/// @nodoc
abstract mixin class $SnHeatmapCopyWith<$Res> {
factory $SnHeatmapCopyWith(SnHeatmap value, $Res Function(SnHeatmap) _then) = _$SnHeatmapCopyWithImpl;
@useResult
$Res call({
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
});
}
/// @nodoc
class _$SnHeatmapCopyWithImpl<$Res>
implements $SnHeatmapCopyWith<$Res> {
_$SnHeatmapCopyWithImpl(this._self, this._then);
final SnHeatmap _self;
final $Res Function(SnHeatmap) _then;
/// Create a copy of SnHeatmap
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
return _then(_self.copyWith(
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as DateTime,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
as List<SnHeatmapItem>,
));
}
}
/// Adds pattern-matching-related methods to [SnHeatmap].
extension SnHeatmapPatterns on SnHeatmap {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnHeatmap value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnHeatmap() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnHeatmap value) $default,){
final _that = this;
switch (_that) {
case _SnHeatmap():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnHeatmap value)? $default,){
final _that = this;
switch (_that) {
case _SnHeatmap() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnHeatmap() when $default != null:
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items) $default,) {final _that = this;
switch (_that) {
case _SnHeatmap():
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,) {final _that = this;
switch (_that) {
case _SnHeatmap() when $default != null:
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnHeatmap implements SnHeatmap {
const _SnHeatmap({required this.unit, @JsonKey(name: 'period_start') required this.periodStart, @JsonKey(name: 'period_end') required this.periodEnd, required final List<SnHeatmapItem> items}): _items = items;
factory _SnHeatmap.fromJson(Map<String, dynamic> json) => _$SnHeatmapFromJson(json);
@override final String unit;
@override@JsonKey(name: 'period_start') final DateTime periodStart;
@override@JsonKey(name: 'period_end') final DateTime periodEnd;
final List<SnHeatmapItem> _items;
@override List<SnHeatmapItem> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
/// Create a copy of SnHeatmap
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnHeatmapCopyWith<_SnHeatmap> get copyWith => __$SnHeatmapCopyWithImpl<_SnHeatmap>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnHeatmapToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other._items, _items));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(_items));
@override
String toString() {
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
}
}
/// @nodoc
abstract mixin class _$SnHeatmapCopyWith<$Res> implements $SnHeatmapCopyWith<$Res> {
factory _$SnHeatmapCopyWith(_SnHeatmap value, $Res Function(_SnHeatmap) _then) = __$SnHeatmapCopyWithImpl;
@override @useResult
$Res call({
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
});
}
/// @nodoc
class __$SnHeatmapCopyWithImpl<$Res>
implements _$SnHeatmapCopyWith<$Res> {
__$SnHeatmapCopyWithImpl(this._self, this._then);
final _SnHeatmap _self;
final $Res Function(_SnHeatmap) _then;
/// Create a copy of SnHeatmap
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
return _then(_SnHeatmap(
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as DateTime,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
as List<SnHeatmapItem>,
));
}
}
/// @nodoc
mixin _$SnHeatmapItem {
DateTime get date; int get count;
/// Create a copy of SnHeatmapItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnHeatmapItemCopyWith<SnHeatmapItem> get copyWith => _$SnHeatmapItemCopyWithImpl<SnHeatmapItem>(this as SnHeatmapItem, _$identity);
/// Serializes this SnHeatmapItem to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,date,count);
@override
String toString() {
return 'SnHeatmapItem(date: $date, count: $count)';
}
}
/// @nodoc
abstract mixin class $SnHeatmapItemCopyWith<$Res> {
factory $SnHeatmapItemCopyWith(SnHeatmapItem value, $Res Function(SnHeatmapItem) _then) = _$SnHeatmapItemCopyWithImpl;
@useResult
$Res call({
DateTime date, int count
});
}
/// @nodoc
class _$SnHeatmapItemCopyWithImpl<$Res>
implements $SnHeatmapItemCopyWith<$Res> {
_$SnHeatmapItemCopyWithImpl(this._self, this._then);
final SnHeatmapItem _self;
final $Res Function(SnHeatmapItem) _then;
/// Create a copy of SnHeatmapItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? count = null,}) {
return _then(_self.copyWith(
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [SnHeatmapItem].
extension SnHeatmapItemPatterns on SnHeatmapItem {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnHeatmapItem value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnHeatmapItem() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnHeatmapItem value) $default,){
final _that = this;
switch (_that) {
case _SnHeatmapItem():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnHeatmapItem value)? $default,){
final _that = this;
switch (_that) {
case _SnHeatmapItem() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, int count)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnHeatmapItem() when $default != null:
return $default(_that.date,_that.count);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, int count) $default,) {final _that = this;
switch (_that) {
case _SnHeatmapItem():
return $default(_that.date,_that.count);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, int count)? $default,) {final _that = this;
switch (_that) {
case _SnHeatmapItem() when $default != null:
return $default(_that.date,_that.count);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnHeatmapItem implements SnHeatmapItem {
const _SnHeatmapItem({required this.date, required this.count});
factory _SnHeatmapItem.fromJson(Map<String, dynamic> json) => _$SnHeatmapItemFromJson(json);
@override final DateTime date;
@override final int count;
/// Create a copy of SnHeatmapItem
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnHeatmapItemCopyWith<_SnHeatmapItem> get copyWith => __$SnHeatmapItemCopyWithImpl<_SnHeatmapItem>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnHeatmapItemToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,date,count);
@override
String toString() {
return 'SnHeatmapItem(date: $date, count: $count)';
}
}
/// @nodoc
abstract mixin class _$SnHeatmapItemCopyWith<$Res> implements $SnHeatmapItemCopyWith<$Res> {
factory _$SnHeatmapItemCopyWith(_SnHeatmapItem value, $Res Function(_SnHeatmapItem) _then) = __$SnHeatmapItemCopyWithImpl;
@override @useResult
$Res call({
DateTime date, int count
});
}
/// @nodoc
class __$SnHeatmapItemCopyWithImpl<$Res>
implements _$SnHeatmapItemCopyWith<$Res> {
__$SnHeatmapItemCopyWithImpl(this._self, this._then);
final _SnHeatmapItem _self;
final $Res Function(_SnHeatmapItem) _then;
/// Create a copy of SnHeatmapItem
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? count = null,}) {
return _then(_SnHeatmapItem(
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on

37
lib/models/heatmap.g.dart Normal file
View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'heatmap.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnHeatmap _$SnHeatmapFromJson(Map<String, dynamic> json) => _SnHeatmap(
unit: json['unit'] as String,
periodStart: DateTime.parse(json['period_start'] as String),
periodEnd: DateTime.parse(json['period_end'] as String),
items:
(json['items'] as List<dynamic>)
.map((e) => SnHeatmapItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$SnHeatmapToJson(_SnHeatmap instance) =>
<String, dynamic>{
'unit': instance.unit,
'period_start': instance.periodStart.toIso8601String(),
'period_end': instance.periodEnd.toIso8601String(),
'items': instance.items.map((e) => e.toJson()).toList(),
};
_SnHeatmapItem _$SnHeatmapItemFromJson(Map<String, dynamic> json) =>
_SnHeatmapItem(
date: DateTime.parse(json['date'] as String),
count: (json['count'] as num).toInt(),
);
Map<String, dynamic> _$SnHeatmapItemToJson(_SnHeatmapItem instance) =>
<String, dynamic>{
'date': instance.date.toIso8601String(),
'count': instance.count,
};

View File

@@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/account.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
@@ -142,3 +143,21 @@ sealed class SnPostAward with _$SnPostAward {
factory SnPostAward.fromJson(Map<String, dynamic> json) =>
_$SnPostAwardFromJson(json);
}
@freezed
sealed class SnPostReaction with _$SnPostReaction {
const factory SnPostReaction({
required String id,
required String symbol,
required int attitude,
required String postId,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
@Default(null) SnAccount? account,
DateTime? deletedAt,
}) = _SnPostReaction;
factory SnPostReaction.fromJson(Map<String, dynamic> json) =>
_$SnPostReactionFromJson(json);
}

View File

@@ -1900,4 +1900,309 @@ as DateTime?,
}
/// @nodoc
mixin _$SnPostReaction {
String get id; String get symbol; int get attitude; String get postId; String get accountId; DateTime get createdAt; DateTime get updatedAt; SnAccount? get account; DateTime? get deletedAt;
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPostReactionCopyWith<SnPostReaction> get copyWith => _$SnPostReactionCopyWithImpl<SnPostReaction>(this as SnPostReaction, _$identity);
/// Serializes this SnPostReaction to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
@override
String toString() {
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnPostReactionCopyWith<$Res> {
factory $SnPostReactionCopyWith(SnPostReaction value, $Res Function(SnPostReaction) _then) = _$SnPostReactionCopyWithImpl;
@useResult
$Res call({
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
});
$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class _$SnPostReactionCopyWithImpl<$Res>
implements $SnPostReactionCopyWith<$Res> {
_$SnPostReactionCopyWithImpl(this._self, this._then);
final SnPostReaction _self;
final $Res Function(SnPostReaction) _then;
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
as int,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable
as String,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,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
/// Adds pattern-matching-related methods to [SnPostReaction].
extension SnPostReactionPatterns on SnPostReaction {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostReaction value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnPostReaction() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostReaction value) $default,){
final _that = this;
switch (_that) {
case _SnPostReaction():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostReaction value)? $default,){
final _that = this;
switch (_that) {
case _SnPostReaction() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnPostReaction() when $default != null:
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnPostReaction():
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnPostReaction() when $default != null:
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnPostReaction implements SnPostReaction {
const _SnPostReaction({required this.id, required this.symbol, required this.attitude, required this.postId, required this.accountId, required this.createdAt, required this.updatedAt, this.account = null, this.deletedAt});
factory _SnPostReaction.fromJson(Map<String, dynamic> json) => _$SnPostReactionFromJson(json);
@override final String id;
@override final String symbol;
@override final int attitude;
@override final String postId;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override@JsonKey() final SnAccount? account;
@override final DateTime? deletedAt;
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPostReactionCopyWith<_SnPostReaction> get copyWith => __$SnPostReactionCopyWithImpl<_SnPostReaction>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPostReactionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
@override
String toString() {
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnPostReactionCopyWith<$Res> implements $SnPostReactionCopyWith<$Res> {
factory _$SnPostReactionCopyWith(_SnPostReaction value, $Res Function(_SnPostReaction) _then) = __$SnPostReactionCopyWithImpl;
@override @useResult
$Res call({
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
});
@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class __$SnPostReactionCopyWithImpl<$Res>
implements _$SnPostReactionCopyWith<$Res> {
__$SnPostReactionCopyWithImpl(this._self, this._then);
final _SnPostReaction _self;
final $Res Function(_SnPostReaction) _then;
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
return _then(_SnPostReaction(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
as int,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable
as String,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,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnPostReaction
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
// dart format on

View File

@@ -230,3 +230,35 @@ Map<String, dynamic> _$SnPostAwardToJson(_SnPostAward instance) =>
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnPostReaction _$SnPostReactionFromJson(Map<String, dynamic> json) =>
_SnPostReaction(
id: json['id'] as String,
symbol: json['symbol'] as String,
attitude: (json['attitude'] as num).toInt(),
postId: json['post_id'] as String,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) =>
<String, dynamic>{
'id': instance.id,
'symbol': instance.symbol,
'attitude': instance.attitude,
'post_id': instance.postId,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'account': instance.account?.toJson(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@@ -320,16 +320,6 @@ class MessagesNotifier extends _$MessagesNotifier {
final response = MessageSyncResponse.fromJson(resp.data);
talker.log('Sync response: ${response.messages.length} changes');
for (final message in response.messages) {
switch (message.type) {
case "messages.update":
case "messages.update.links":
await receiveMessageUpdate(message);
break;
case "messages.delete":
await receiveMessageDeletion(message.id.toString());
break;
}
// Still need receive the message to show the history actions
await receiveMessage(message);
}
} catch (err, stackTrace) {
@@ -705,6 +695,8 @@ class MessagesNotifier extends _$MessagesNotifier {
remoteMessage.copyWith(
id: targetId,
meta: Map.of(remoteMessage.meta)..remove('message_id'),
type: 'text',
editedAt: remoteMessage.createdAt,
),
MessageStatus.sent,
);

View File

@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'639286fd8e4e0cfdef5be6cf5d80eea769007cb3';
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -34,6 +34,7 @@ const kAppDefaultPoolId = 'app_default_pool_id';
const kAppMessageDisplayStyle = 'app_message_display_style';
const kAppThemeMode = 'app_theme_mode';
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
const kAppDisableAnimation = 'app_disable_animation';
const kFeaturedPostsCollapsedId =
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
@@ -96,6 +97,7 @@ sealed class AppSettings with _$AppSettings {
required String messageDisplayStyle,
required String? themeMode,
required bool useMaterial3,
required bool disableAnimation,
}) = _AppSettings;
}
@@ -122,6 +124,7 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
useMaterial3: prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
disableAnimation: prefs.getBool(kAppDisableAnimation) ?? false,
);
}
@@ -273,6 +276,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
}
state = state.copyWith(customColors: value);
}
void setDisableAnimation(bool value) {
final prefs = ref.read(sharedPreferencesProvider);
prefs.setBool(kAppDisableAnimation, value);
state = state.copyWith(disableAnimation: value);
}
}
final updateInfoProvider =

View File

@@ -290,7 +290,7 @@ mixin _$AppSettings {
ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms
double get windowOpacity;// The window opacity for desktop platforms
double get cardTransparency;// The card background opacity
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3;
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3; bool get disableAnimation;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -301,16 +301,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)';
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
}
@@ -321,7 +321,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
});
@@ -338,7 +338,7 @@ class _$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -357,6 +357,7 @@ as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaul
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@@ -451,10 +452,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
return orElse();
}
@@ -472,10 +473,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation) $default,) {final _that = this;
switch (_that) {
case _AppSettings():
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);}
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -489,10 +490,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,) {final _that = this;
switch (_that) {
case _AppSettings() when $default != null:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3);case _:
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
return null;
}
@@ -504,7 +505,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings {
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3});
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3, required this.disableAnimation});
@override final bool autoTranslate;
@@ -528,6 +529,7 @@ class _AppSettings implements AppSettings {
@override final String messageDisplayStyle;
@override final String? themeMode;
@override final bool useMaterial3;
@override final bool disableAnimation;
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@@ -539,16 +541,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
}
@override
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3);
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
@override
String toString() {
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3)';
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
}
@@ -559,7 +561,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult
$Res call({
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
});
@@ -576,7 +578,7 @@ class __$AppSettingsCopyWithImpl<$Res>
/// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
@@ -595,6 +597,7 @@ as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaul
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -30,7 +30,7 @@ Map<String, dynamic> _$ThemeColorsToJson(_ThemeColors instance) =>
// **************************************************************************
String _$appSettingsNotifierHash() =>
r'3ba2cdce76f3c4fed84f4108341c88a0a971bf3a';
r'10ebd893c39d24ae588a4c0bf4144ee4656465a4';
/// See also [AppSettingsNotifier].
@ProviderFor(AppSettingsNotifier)

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
@@ -109,6 +110,16 @@ final apiClientProvider = Provider<Dio>((ref) {
printResponseData: false,
),
),
RetryInterceptor(
dio: dio,
retries: 3,
retryDelays: const [
Duration(seconds: 1),
Duration(seconds: 2),
Duration(seconds: 3),
],
retryEvaluator: (err, _) => err.requestOptions.method == 'GET',
),
]);
return dio;

View File

@@ -58,7 +58,6 @@ import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/realm_form.dart';
import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart';
@@ -138,193 +137,6 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/logs',
builder: (context, state) => TalkerScreen(talker: talker),
),
GoRoute(
name: 'accountCalendar',
path: '/account/:name/calendar',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EventCalanderScreen(name: name);
},
),
GoRoute(
name: 'creatorHub',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
routes: [
// Web Feed Routes
GoRoute(
name: 'creatorFeeds',
path: ':name/feeds',
builder: (context, state) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
),
GoRoute(
name: 'creatorPosts',
path: ':name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
// Poll list route
GoRoute(
name: 'creatorPolls',
path: ':name/polls',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPollListScreen(pubName: name);
},
),
// Poll routes
GoRoute(
name: 'creatorPollNew',
path: ':name/polls/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
// initialPollId left null for create; initialPublisher prefilled
return PollEditorScreen(initialPublisher: name);
},
),
GoRoute(
name: 'creatorPollEdit',
path: ':name/polls/:id/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final id = state.pathParameters['id']!;
return PollEditorScreen(
initialPollId: id,
initialPublisher: name,
);
},
),
GoRoute(
name: 'creatorStickers',
path: ':name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
name: 'creatorNew',
path: 'new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
name: 'creatorEdit',
path: ':name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
},
),
],
),
GoRoute(
name: 'developerHub',
path: '/developers',
builder:
(context, state) => DeveloperHubScreen(
initialPublisherName: state.uri.queryParameters['publisher'],
initialProjectId: state.uri.queryParameters['project'],
),
routes: [
GoRoute(
name: 'developerProjectNew',
path: ':name/projects/new',
builder:
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerProjectEdit',
path: ':name/projects/:id/edit',
builder:
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerProjectDetail',
path: ':name/projects/:projectId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final projectId = state.pathParameters['projectId']!;
// Redirect to hub with project selected
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go(
'/developers?publisher=$name&project=$projectId',
);
});
return const SizedBox.shrink(); // Temporary placeholder
},
routes: [
GoRoute(
name: 'developerAppNew',
path: 'apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: 'apps/:id/edit',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerAppDetail',
path: 'apps/:appId',
builder:
(context, state) => AppDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
appId: state.pathParameters['appId']!,
),
),
GoRoute(
name: 'developerBotNew',
path: 'bots/new',
builder:
(context, state) => NewBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerBotDetail',
path: 'bots/:botId',
builder:
(context, state) => BotDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
botId: state.pathParameters['botId']!,
),
),
GoRoute(
name: 'developerBotEdit',
path: 'bots/:id/edit',
builder:
(context, state) => EditBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
],
),
],
),
// Web articles
GoRoute(
@@ -633,12 +445,195 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
name: 'accountProfile',
path: '/account/:name',
path: '/accounts/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
// Creator hub tab
GoRoute(
name: 'creatorHub',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
routes: [
// Web Feed Routes
GoRoute(
name: 'creatorFeeds',
path: ':name/feeds',
builder: (context, state) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
),
GoRoute(
name: 'creatorPosts',
path: ':name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
// Poll list route
GoRoute(
name: 'creatorPolls',
path: ':name/polls',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPollListScreen(pubName: name);
},
),
// Poll routes
GoRoute(
name: 'creatorPollNew',
path: ':name/polls/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
// initialPollId left null for create; initialPublisher prefilled
return PollEditorScreen(initialPublisher: name);
},
),
GoRoute(
name: 'creatorPollEdit',
path: ':name/polls/:id/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final id = state.pathParameters['id']!;
return PollEditorScreen(
initialPollId: id,
initialPublisher: name,
);
},
),
GoRoute(
name: 'creatorStickers',
path: ':name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
name: 'creatorNew',
path: 'new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
name: 'creatorEdit',
path: ':name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
},
),
],
),
// Developer hub tab
GoRoute(
name: 'developerHub',
path: '/developers',
builder:
(context, state) => DeveloperHubScreen(
initialPublisherName:
state.uri.queryParameters['publisher'],
initialProjectId: state.uri.queryParameters['project'],
),
routes: [
GoRoute(
name: 'developerProjectNew',
path: ':name/projects/new',
builder:
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerProjectEdit',
path: ':name/projects/:id/edit',
builder:
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerProjectDetail',
path: ':name/projects/:projectId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final projectId = state.pathParameters['projectId']!;
// Redirect to hub with project selected
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go(
'/developers?publisher=$name&project=$projectId',
);
});
return const SizedBox.shrink(); // Temporary placeholder
},
routes: [
GoRoute(
name: 'developerAppNew',
path: 'apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: 'apps/:id/edit',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerAppDetail',
path: 'apps/:appId',
builder:
(context, state) => AppDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
appId: state.pathParameters['appId']!,
),
),
GoRoute(
name: 'developerBotNew',
path: 'bots/new',
builder:
(context, state) => NewBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerBotDetail',
path: 'bots/:botId',
builder:
(context, state) => BotDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
botId: state.pathParameters['botId']!,
),
),
GoRoute(
name: 'developerBotEdit',
path: 'bots/:id/edit',
builder:
(context, state) => EditBotScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
id: state.pathParameters['id']!,
),
),
],
),
],
),
],
),
],

View File

@@ -149,132 +149,124 @@ class AccountScreen extends HookConsumerWidget {
context.pushNamed('leveling');
},
).padding(horizontal: 12),
if (!isWideScreen(context)) const SizedBox.shrink(),
if (!isWideScreen(context))
Row(
spacing: 8,
children: [
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.draw, size: 28).padding(bottom: 8),
Text(
'creatorHub',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'creatorHubDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.goNamed('creatorHub');
},
),
).height(140),
),
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.code, size: 28).padding(bottom: 8),
Text(
'developerPortal',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'developerPortalDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('developerHub');
},
),
).height(140),
),
],
).padding(horizontal: 12),
const SizedBox.shrink(),
Row(
spacing: 8,
children: [
Expanded(
child: Card(
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
spacing: 8,
children: [
Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
spacing: 8,
children: [
Icon(Symbols.draw, size: 28).padding(bottom: 8),
Text(
'creatorHub',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'creatorHubDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.goNamed('creatorHub');
},
),
).height(140),
),
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.code, size: 28).padding(bottom: 8),
Text(
'developerPortal',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr().fontSize(16).bold(),
Text(
'developerPortalDescription',
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('developerHub');
},
),
).height(140),
),
],
).padding(horizontal: 12),
const SizedBox.shrink(),
Row(
spacing: 8,
children: [
Expanded(
child: Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.settings, size: 28).padding(bottom: 8),
Text('appSettings').tr().fontSize(16).bold(),
Icon(Symbols.settings, size: 20),
Text('appSettings').tr().fontSize(13).bold(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('settings');
},
),
).height(120),
),
Expanded(
child: Card(
),
Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
spacing: 8,
children: [
Icon(
Symbols.person_edit,
size: 28,
).padding(bottom: 8),
Text('updateYourProfile').tr().fontSize(16).bold(),
Icon(Symbols.person_edit, size: 20),
Text('updateYourProfile').tr().fontSize(13).bold(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('profileUpdate');
},
),
).height(120),
),
Expanded(
child: Card(
),
Card(
margin: EdgeInsets.zero,
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
spacing: 8,
children: [
Icon(
Symbols.manage_accounts,
size: 28,
).padding(bottom: 8),
Text('accountSettings').tr().fontSize(16).bold(),
Icon(Symbols.manage_accounts, size: 20),
Text('accountSettings').tr().fontSize(13).bold(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {
context.pushNamed('accountSettings');
},
),
).height(120),
),
],
).padding(horizontal: 12),
),
],
).padding(horizontal: 12),
).height(48),
ListTile(
minTileHeight: 48,
leading: const Icon(Symbols.notifications),

View File

@@ -14,6 +14,7 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/timezone.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
@@ -24,6 +25,17 @@ const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'};
const kServerSupportedRegions = ['US', 'JP', 'CN'];
class UpdateProfileScreen extends HookConsumerWidget {
bool _isValidHexColor(String color) {
if (!color.startsWith('#')) return false;
if (color.length != 7) return false; // #RRGGBB format
try {
int.parse(color.substring(1), radix: 16);
return true;
} catch (_) {
return false;
}
}
const UpdateProfileScreen({super.key});
@override
@@ -148,12 +160,39 @@ class UpdateProfileScreen extends HookConsumerWidget {
text: user.value!.profile.timeZone,
);
// Username color state
final usernameColorType = useState<String>(
user.value!.profile.usernameColor?.type ?? 'plain',
);
final usernameColorValue = useTextEditingController(
text: user.value!.profile.usernameColor?.value,
);
final usernameColorDirection = useTextEditingController(
text: user.value!.profile.usernameColor?.direction,
);
final usernameColorColors = useState<List<String>>(
user.value!.profile.usernameColor?.colors ?? [],
);
void updateProfile() async {
if (!formKeyProfile.currentState!.validate()) return;
submitting.value = true;
try {
final client = ref.watch(apiClientProvider);
final usernameColorData = {
'type': usernameColorType.value,
if (usernameColorType.value == 'plain' &&
usernameColorValue.text.isNotEmpty)
'value': usernameColorValue.text,
if (usernameColorType.value == 'gradient') ...{
if (usernameColorDirection.text.isNotEmpty)
'direction': usernameColorDirection.text,
'colors':
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
},
};
await client.patch(
'/id/accounts/me/profile',
data: {
@@ -166,6 +205,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'location': locationController.text,
'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(),
'username_color': usernameColorData,
'links':
links.value
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
@@ -593,6 +633,320 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
),
),
Text(
'usernameColor',
).tr().bold().fontSize(18).padding(top: 16),
Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Preview section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('preview').tr().bold().fontSize(14),
const Gap(8),
// Create a preview account with the current settings
Builder(
builder: (context) {
final previewAccount = user.value!.copyWith(
profile: user.value!.profile.copyWith(
usernameColor: UsernameColor(
type: usernameColorType.value,
value:
usernameColorType.value == 'plain' &&
usernameColorValue
.text
.isNotEmpty
? usernameColorValue.text
: null,
direction:
usernameColorType.value ==
'gradient' &&
usernameColorDirection
.text
.isNotEmpty
? usernameColorDirection.text
: null,
colors:
usernameColorType.value == 'gradient'
? usernameColorColors.value
.where((c) => c.isNotEmpty)
.toList()
: null,
),
),
);
// Check if user can use this color
final tier =
user.value!.perkSubscription?.identifier;
final canUseColor = switch (tier) {
'solian.stellar.primary' =>
usernameColorType.value == 'plain' &&
(kUsernamePlainColors.containsKey(
usernameColorValue.text,
) ||
(usernameColorValue.text.startsWith(
'#',
) &&
_isValidHexColor(
usernameColorValue.text,
))),
'solian.stellar.nova' =>
usernameColorType.value == 'plain',
'solian.stellar.supernova' => true,
_ => false,
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountName(
account: previewAccount,
style: const TextStyle(fontSize: 18),
ignorePermissions: true,
),
const Gap(4),
Row(
children: [
Icon(
canUseColor
? Symbols.check_circle
: Symbols.error,
size: 16,
color:
canUseColor
? Colors.green
: Colors.red,
),
const Gap(4),
Text(
canUseColor
? 'availableWithYourPlan'.tr()
: 'upgradeRequired'.tr(),
style: TextStyle(
fontSize: 12,
color:
canUseColor
? Colors.green
: Colors.red,
),
),
],
),
],
);
},
),
],
),
),
DropdownButtonFormField2<String>(
decoration: InputDecoration(
labelText: 'colorType'.tr(),
),
items: [
DropdownMenuItem(
value: 'plain',
child: Text('plain'.tr()),
),
DropdownMenuItem(
value: 'gradient',
child: Text('gradient'.tr()),
),
],
value: usernameColorType.value,
onChanged: (value) {
usernameColorType.value = value ?? 'plain';
},
customButton: Row(
children: [
Expanded(child: Text(usernameColorType.value).tr()),
Icon(Symbols.arrow_drop_down),
],
),
),
if (usernameColorType.value == 'plain')
Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
final options = kUsernamePlainColors.keys.toList();
if (textEditingValue.text == '') {
return options;
}
return options.where(
(option) => option.toLowerCase().contains(
textEditingValue.text.toLowerCase(),
),
);
},
onSelected: (String selection) {
usernameColorValue.text = selection;
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
// Initialize the controller with the current value
if (controller.text.isEmpty &&
usernameColorValue.text.isNotEmpty) {
controller.text = usernameColorValue.text;
}
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'colorValue'.tr(),
hintText: 'e.g. red or #ff6600',
),
onChanged: (value) {
usernameColorValue.text = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
);
},
),
if (usernameColorType.value == 'gradient') ...[
DropdownButtonFormField2<String>(
decoration: InputDecoration(
labelText: 'gradientDirection'.tr(),
),
items: [
DropdownMenuItem(
value: 'to right',
child: Text('gradientDirectionToRight'.tr()),
),
DropdownMenuItem(
value: 'to left',
child: Text('gradientDirectionToLeft'.tr()),
),
DropdownMenuItem(
value: 'to bottom',
child: Text('gradientDirectionToBottom'.tr()),
),
DropdownMenuItem(
value: 'to top',
child: Text('gradientDirectionToTop'.tr()),
),
DropdownMenuItem(
value: 'to bottom right',
child: Text(
'gradientDirectionToBottomRight'.tr(),
),
),
DropdownMenuItem(
value: 'to bottom left',
child: Text('gradientDirectionToBottomLeft'.tr()),
),
DropdownMenuItem(
value: 'to top right',
child: Text('gradientDirectionToTopRight'.tr()),
),
DropdownMenuItem(
value: 'to top left',
child: Text('gradientDirectionToTopLeft'.tr()),
),
],
value:
usernameColorDirection.text.isNotEmpty
? usernameColorDirection.text
: 'to right',
onChanged: (value) {
usernameColorDirection.text = value ?? 'to right';
},
customButton: Row(
children: [
Expanded(
child: Text(
usernameColorDirection.text.isNotEmpty
? usernameColorDirection.text
: 'to right',
),
),
Icon(Symbols.arrow_drop_down),
],
),
),
Text(
'gradientColors',
).tr().bold().fontSize(14).padding(top: 8),
Column(
spacing: 8,
children: [
for (
var i = 0;
i < usernameColorColors.value.length;
i++
)
Row(
key: ValueKey(
usernameColorColors.value[i].hashCode,
),
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextFormField(
initialValue:
usernameColorColors.value[i],
decoration: InputDecoration(
labelText: 'color'.tr(),
hintText: 'e.g. #ff0000',
isDense: true,
),
onChanged: (value) {
usernameColorColors.value[i] = value;
},
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
),
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
usernameColorColors.value =
usernameColorColors.value
.whereIndexed(
(idx, _) => idx != i,
)
.toList();
},
),
],
),
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed: () {
usernameColorColors.value = List.from(
usernameColorColors.value,
)..add('');
},
label: Text('addColor').tr(),
icon: const Icon(Symbols.add),
).padding(top: 8),
),
],
),
],
],
),
Text('links').tr().bold().fontSize(18).padding(top: 16),
Column(
spacing: 8,

View File

@@ -97,9 +97,7 @@ class _AccountBasicInfo extends StatelessWidget {
IconButton(
onPressed: () {
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('https://solian.app/@${data.name}'),
),
ShareParams(uri: Uri.parse('https://solian.app/@${data.name}')),
);
},
icon: const Icon(Symbols.share),
@@ -879,7 +877,7 @@ class AccountProfileScreen extends HookConsumerWidget {
child: Card(
child: FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
eventCalandarUser: data.name,
margin: EdgeInsets.zero,
),
),
@@ -1004,7 +1002,7 @@ class AccountProfileScreen extends HookConsumerWidget {
child: Card(
child: FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
eventCalandarUser: data.name,
),
).padding(horizontal: 4),
),

View File

@@ -14,6 +14,7 @@ import "package:island/pods/chat/chat_subscribe.dart";
import "package:island/pods/chat/messages_notifier.dart";
import "package:island/pods/network.dart";
import "package:island/pods/chat/chat_online_count.dart";
import "package:island/pods/config.dart";
import "package:island/screens/chat/search_messages.dart";
import "package:island/services/file_uploader.dart";
import "package:island/screens/chat/chat.dart";
@@ -44,6 +45,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final chatIdentity = ref.watch(chatroomIdentityProvider(id));
final isSyncing = ref.watch(isSyncingProvider);
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
final settings = ref.watch(appSettingsNotifierProvider);
final hasOnlineCount = onlineCount.hasValue;
@@ -535,7 +537,7 @@ class ChatRoomScreen extends HookConsumerWidget {
listController: listController,
padding: EdgeInsets.only(
top: 16,
bottom: 80 + MediaQuery.of(context).padding.bottom,
bottom: MediaQuery.of(context).padding.bottom + 16,
),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
@@ -543,7 +545,9 @@ class ChatRoomScreen extends HookConsumerWidget {
findChildIndexCallback: (key) {
if (key is! ValueKey<String>) return null;
final messageId = key.value.substring(messageKeyPrefix.length);
final index = messageList.indexWhere((m) => m.id == messageId);
final index = messageList.indexWhere(
(m) => (m.nonce ?? m.id) == messageId,
);
// Return null for invalid indices to let SuperListView handle it properly
return index >= 0 ? index : null;
},
@@ -561,13 +565,14 @@ class ChatRoomScreen extends HookConsumerWidget {
.abs() >
3;
final key = ValueKey('$messageKeyPrefix${message.id}');
// Use a stable animation key that doesn't change during message lifecycle
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
return chatIdentity.when(
final messageWidget = chatIdentity.when(
skipError: true,
data:
(identity) => MessageItem(
key: key,
key: settings.disableAnimation ? key : null,
message: message,
isCurrentUser: identity?.id == message.senderId,
onAction: (action) {
@@ -605,7 +610,6 @@ class ChatRoomScreen extends HookConsumerWidget {
),
loading:
() => MessageItem(
key: key,
message: message,
isCurrentUser: false,
onAction: null,
@@ -613,8 +617,29 @@ class ChatRoomScreen extends HookConsumerWidget {
showAvatar: false,
onJump: (_) {},
),
error: (_, _) => SizedBox.shrink(key: key),
error: (_, _) => const SizedBox.shrink(),
);
return settings.disableAnimation
? messageWidget
: TweenAnimationBuilder<double>(
key: key,
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: Duration(
milliseconds: 400 + (index % 5) * 50,
), // Staggered delay
curve: Curves.easeOutCubic,
builder: (context, animationValue, child) {
return Transform.translate(
offset: Offset(
0,
20 * (1 - animationValue),
), // Slide up from bottom
child: Opacity(opacity: animationValue, child: child),
);
},
child: messageWidget,
);
},
);
@@ -687,91 +712,116 @@ class ChatRoomScreen extends HookConsumerWidget {
),
body: Stack(
children: [
// Messages
// Messages and Input in Column
Positioned.fill(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
child: Column(
children: [
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (
Widget child,
Animation<double> animation,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(
key: const ValueKey('empty-messages'),
child: Text('No messages yet'.tr()),
)
: chatMessageListWidget(messageList),
loading:
() => const Center(
key: ValueKey('loading-messages'),
child: CircularProgressIndicator(),
),
error:
(error, _) => ResponseErrorWidget(
key: const ValueKey('error-messages'),
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
),
),
// Input
Positioned(
bottom: 0,
left: 0,
right: 0,
child: chatRoom.when(
data:
(room) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
chatRoom.when(
data:
(room) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ChatInput(
messageController: messageController,
chatRoom: room!,
onSend: sendMessage,
onClear: () {
if (messageEditingTo.value != null) {
attachments.value.clear();
messageController.clear();
}
messageEditingTo.value = null;
messageReplyingTo.value = null;
messageForwardingTo.value = null;
},
messageEditingTo: messageEditingTo.value,
messageReplyingTo: messageReplyingTo.value,
messageForwardingTo: messageForwardingTo.value,
onPickFile: (bool isPhoto) {
if (isPhoto) {
pickPhotoMedia();
} else {
pickVideoMedia();
}
},
onPickAudio: pickAudioMedia,
onPickGeneralFile: pickGeneralFile,
onLinkAttachment: linkAttachment,
attachments: attachments.value,
onUploadAttachment: uploadAttachment,
onDeleteAttachment: (index) async {
final attachment = attachments.value[index];
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete(
'/drive/files/${attachment.data.id}',
);
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
},
onMoveAttachment: (idx, delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(idx + delta, clone.removeAt(idx));
attachments.value = clone;
},
onAttachmentsChanged: (newAttachments) {
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
),
],
),
),
Positioned(

View File

@@ -8,6 +8,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/heatmap.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/services/responsive.dart';
@@ -18,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:island/widgets/activity_heatmap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@@ -33,6 +35,14 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
return SnPublisherStats.fromJson(resp.data);
}
@riverpod
Future<SnHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
if (uname == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
return SnHeatmap.fromJson(resp.data);
}
@riverpod
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
try {
@@ -329,6 +339,10 @@ class CreatorHubScreen extends HookConsumerWidget {
publisherStatsProvider(currentPublisher.value?.name),
);
final publisherHeatmap = ref.watch(
publisherHeatmapProvider(currentPublisher.value?.name),
);
final publisherFeatures = ref.watch(
publisherFeaturesProvider(currentPublisher.value?.name),
);
@@ -557,6 +571,7 @@ class CreatorHubScreen extends HookConsumerWidget {
if (stats != null)
_PublisherStatsWidget(
stats: stats,
heatmap: publisherHeatmap.value,
).padding(horizontal: 12),
buildNavigationWidget(true),
],
@@ -567,6 +582,7 @@ class CreatorHubScreen extends HookConsumerWidget {
if (stats != null)
_PublisherStatsWidget(
stats: stats,
heatmap: publisherHeatmap.value,
).padding(horizontal: 16),
buildNavigationWidget(false),
],
@@ -585,7 +601,8 @@ class CreatorHubScreen extends HookConsumerWidget {
class _PublisherStatsWidget extends StatelessWidget {
final SnPublisherStats stats;
const _PublisherStatsWidget({required this.stats});
final SnHeatmap? heatmap;
const _PublisherStatsWidget({required this.stats, this.heatmap});
@override
Widget build(BuildContext context) {
@@ -638,6 +655,7 @@ class _PublisherStatsWidget extends StatelessWidget {
),
],
),
if (heatmap != null) ActivityHeatmapWidget(heatmap: heatmap!),
],
),
);

View File

@@ -149,6 +149,127 @@ class _PublisherStatsProviderElement
String? get uname => (origin as PublisherStatsProvider).uname;
}
String _$publisherHeatmapHash() => r'5f70c55e14629ec8628445a317888e02fccd9af2';
/// See also [publisherHeatmap].
@ProviderFor(publisherHeatmap)
const publisherHeatmapProvider = PublisherHeatmapFamily();
/// See also [publisherHeatmap].
class PublisherHeatmapFamily extends Family<AsyncValue<SnHeatmap?>> {
/// See also [publisherHeatmap].
const PublisherHeatmapFamily();
/// See also [publisherHeatmap].
PublisherHeatmapProvider call(String? uname) {
return PublisherHeatmapProvider(uname);
}
@override
PublisherHeatmapProvider getProviderOverride(
covariant PublisherHeatmapProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'publisherHeatmapProvider';
}
/// See also [publisherHeatmap].
class PublisherHeatmapProvider extends AutoDisposeFutureProvider<SnHeatmap?> {
/// See also [publisherHeatmap].
PublisherHeatmapProvider(String? uname)
: this._internal(
(ref) => publisherHeatmap(ref as PublisherHeatmapRef, uname),
from: publisherHeatmapProvider,
name: r'publisherHeatmapProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherHeatmapHash,
dependencies: PublisherHeatmapFamily._dependencies,
allTransitiveDependencies:
PublisherHeatmapFamily._allTransitiveDependencies,
uname: uname,
);
PublisherHeatmapProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String? uname;
@override
Override overrideWith(
FutureOr<SnHeatmap?> Function(PublisherHeatmapRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PublisherHeatmapProvider._internal(
(ref) => create(ref as PublisherHeatmapRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnHeatmap?> createElement() {
return _PublisherHeatmapProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherHeatmapProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnHeatmap?> {
/// The parameter `uname` of this provider.
String? get uname;
}
class _PublisherHeatmapProviderElement
extends AutoDisposeFutureProviderElement<SnHeatmap?>
with PublisherHeatmapRef {
_PublisherHeatmapProviderElement(super.provider);
@override
String? get uname => (origin as PublisherHeatmapProvider).uname;
}
String _$publisherIdentityHash() => r'299372f25fa4b2bf8e11a8ba2d645100fc38e76f';
/// See also [publisherIdentity].

View File

@@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
@@ -32,6 +33,7 @@ class AppDetailScreen extends HookConsumerWidget {
);
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(appData.value?.name ?? 'appDetails'.tr()),
actions: [
@@ -51,6 +53,7 @@ class AppDetailScreen extends HookConsumerWidget {
);
},
),
const Gap(8),
],
bottom: TabBar(
controller: tabController,

View File

@@ -30,6 +30,7 @@ class BotDetailScreen extends HookConsumerWidget {
final botData = ref.watch(botProvider(publisherName, projectId, botId));
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
actions: [

View File

@@ -317,6 +317,7 @@ class EditAppScreen extends HookConsumerWidget {
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
),

View File

@@ -192,6 +192,7 @@ class EditBotScreen extends HookConsumerWidget {
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
body:
botData == null && !isNew

View File

@@ -1,9 +1,10 @@
import 'dart:math';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/dev_project.dart';
import 'package:island/models/developer.dart';
@@ -20,6 +21,7 @@ import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'hub.g.dart';
@@ -64,7 +66,6 @@ class DeveloperHubScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnDeveloper?>(
developers.value?.firstOrNull,
@@ -87,274 +88,398 @@ class DeveloperHubScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('Solar Network Cloud'),
actions: [
appBar: _ConsoleAppBar(
currentDeveloper: currentDeveloper.value,
currentProject: currentProject.value,
onProjectChanged: (value) {
currentProject.value = value;
},
onDeveloperChanged: (value) {
currentDeveloper.value = value;
},
),
body: Column(
children: [
if (currentProject.value == null)
...([
// Welcome Section
_WelcomeSection(currentDeveloper: currentDeveloper.value),
// Navigation Tabs
_NavigationTabs(),
]),
// Main Content
if (currentProject.value != null)
ProjectSelector(
Expanded(
child: ProjectDetailView(
publisherName: currentDeveloper.value!.publisher!.name,
project: currentProject.value!,
onBackToHub: () {
currentProject.value = null;
},
),
)
else
_MainContentSection(
currentDeveloper: currentDeveloper.value,
currentProject: currentProject.value,
onProjectChanged: (value) {
currentProject.value = value;
projects: projects,
developerStats: developerStats,
onProjectSelected: (project) {
currentProject.value = project;
},
onDeveloperSelected: (developer) {
currentDeveloper.value = developer;
},
onCreateProject: () {
if (currentDeveloper.value != null) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'createProject'.tr(),
child: ProjectForm(
publisherName:
currentDeveloper.value!.publisher!.name,
),
),
).then((value) {
if (value != null) {
ref.invalidate(
devProjectsProvider(
currentDeveloper.value!.publisher!.name,
),
);
}
});
}
},
),
if (!isWide)
DeveloperSelector(
isReadOnly: false,
currentDeveloper: currentDeveloper.value,
onDeveloperChanged: (value) {
currentDeveloper.value = value;
},
),
const Gap(8),
],
),
body: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = isWide ? 800.0 : double.infinity;
);
}
}
return Center(
child:
currentProject.value != null
? ProjectDetailView(
publisherName: currentDeveloper.value!.publisher!.name,
project: currentProject.value!,
onBackToHub: () {
currentProject.value = null;
},
class _ConsoleAppBar extends StatelessWidget implements PreferredSizeWidget {
final SnDeveloper? currentDeveloper;
final DevProject? currentProject;
final ValueChanged<DevProject?> onProjectChanged;
final ValueChanged<SnDeveloper?> onDeveloperChanged;
const _ConsoleAppBar({
required this.currentDeveloper,
required this.currentProject,
required this.onProjectChanged,
required this.onDeveloperChanged,
});
@override
Size get preferredSize => const Size.fromHeight(56);
@override
Widget build(BuildContext context) {
return AppBar(
leading: const PageBackButton(),
title: Text('developerHub').tr(),
actions: [
if (currentProject != null)
ProjectSelector(
currentDeveloper: currentDeveloper,
currentProject: currentProject,
onProjectChanged: onProjectChanged,
),
IconButton(
icon: const Icon(Symbols.help, color: Color(0xFF5F6368)),
onPressed: () {
launchUrlString('https://kb.solsynth.dev');
},
),
const Gap(12),
],
);
}
}
// Welcome Section
class _WelcomeSection extends StatelessWidget {
final SnDeveloper? currentDeveloper;
const _WelcomeSection({required this.currentDeveloper});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors:
isDark
? [
Theme.of(context).colorScheme.surfaceContainerHighest,
Theme.of(context).colorScheme.surfaceContainerLow,
]
: [const Color(0xFFE8F0FE), const Color(0xFFF1F3F4)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
Positioned(
right: 16,
top: 0,
bottom: 0,
child: _RandomStickerImage(
width: 180,
height: 180,
).opacity(isWideScreen(context) ? 1 : 0.5),
),
Container(
height: 180,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Good morning!',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
const Gap(4),
Text(
currentDeveloper != null
? "You're working as ${currentDeveloper!.publisher!.nick}"
: "Choose a developer and continue.",
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
);
}
}
// Random Sticker Image Widget
class _RandomStickerImage extends StatelessWidget {
final double? width;
final double? height;
const _RandomStickerImage({this.width, this.height});
static const List<String> _stickers = [
'assets/images/stickers/clap.png',
'assets/images/stickers/confuse.png',
'assets/images/stickers/pray.png',
'assets/images/stickers/thumb_up.png',
];
String _getRandomSticker() {
final random = Random();
return _stickers[random.nextInt(_stickers.length)];
}
@override
Widget build(BuildContext context) {
return Image.asset(
_getRandomSticker(),
width: width ?? 80,
height: height ?? 80,
fit: BoxFit.contain,
);
}
}
// Navigation Tabs
class _NavigationTabs extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
child: Row(
children: [
const Gap(24),
_NavTabItem(title: 'Dashboard', isActive: true),
],
),
);
}
}
class _NavTabItem extends StatelessWidget {
final String title;
final bool isActive;
const _NavTabItem({required this.title, this.isActive = false});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color:
isActive
? Theme.of(context).colorScheme.primary
: Colors.transparent,
width: 2,
),
),
),
child: Row(
children: [
Text(
title,
style: TextStyle(
color:
isActive
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
fontWeight: isActive ? FontWeight.w500 : FontWeight.w400,
),
),
],
),
);
}
}
// Main Content Section
class _MainContentSection extends HookConsumerWidget {
final SnDeveloper? currentDeveloper;
final AsyncValue<List<DevProject>> projects;
final AsyncValue<DeveloperStats?> developerStats;
final ValueChanged<DevProject> onProjectSelected;
final ValueChanged<SnDeveloper> onDeveloperSelected;
final VoidCallback onCreateProject;
const _MainContentSection({
required this.currentDeveloper,
required this.projects,
required this.developerStats,
required this.onProjectSelected,
required this.onDeveloperSelected,
required this.onCreateProject,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
padding: const EdgeInsets.all(8),
child: developerStats.when(
data:
(stats) =>
currentDeveloper == null
? _DeveloperUnselectedWidget(
onDeveloperSelected: onDeveloperSelected,
)
: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: developerStats.when(
data:
(stats) => SingleChildScrollView(
child:
currentDeveloper.value == null
? ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 640,
),
child: _DeveloperUnselectedWidget(
onDeveloperSelected: (developer) {
currentDeveloper.value = developer;
},
),
).center()
: isWide
? Column(
spacing: 8,
children: [
DeveloperSelector(
isReadOnly: true,
currentDeveloper:
currentDeveloper.value,
onDeveloperChanged: (value) {
currentDeveloper.value = value;
},
),
if (stats != null)
_DeveloperStatsWidget(
stats: stats,
).padding(horizontal: 12),
Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(
16,
),
child: Row(
children: [
const Icon(
Symbols.folder_code,
),
const Gap(12),
Text(
'projects',
style:
Theme.of(context)
.textTheme
.titleMedium,
).tr(),
const Spacer(),
IconButton(
visualDensity:
VisualDensity
.compact,
icon: const Icon(
Symbols.add,
),
onPressed: () {
context.pushNamed(
'developerProjectNew',
pathParameters: {
'name':
currentDeveloper
.value!
.publisher!
.name,
},
);
},
),
],
),
),
if (projects
.value
?.isNotEmpty ??
false)
...(projects.value?.map(
(
project,
) => _ProjectListTile(
project: project,
publisherName:
currentDeveloper
.value!
.publisher!
.name,
onProjectSelected: (
selectedProject,
) {
currentProject
.value =
selectedProject;
},
),
) ??
[])
else
Padding(
padding:
const EdgeInsets.all(
16,
),
child: Center(
child:
Text(
'noProjects',
).tr(),
),
),
],
),
),
],
)
: Column(
spacing: 12,
children: [
if (stats != null)
_DeveloperStatsWidget(
stats: stats,
).padding(horizontal: 16),
Card(
margin: const EdgeInsets.symmetric(
horizontal: 16,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(
16,
),
child: Row(
children: [
Text(
'projects',
style:
Theme.of(context)
.textTheme
.titleMedium,
).tr(),
const Spacer(),
IconButton(
icon: const Icon(
Symbols.add,
),
onPressed: () {
context.pushNamed(
'developerProjectNew',
pathParameters: {
'name':
currentDeveloper
.value!
.publisher!
.name,
},
);
},
),
],
),
),
if (projects
.value
?.isNotEmpty ??
false)
...(projects.value?.map(
(
project,
) => _ProjectListTile(
project: project,
publisherName:
currentDeveloper
.value!
.publisher!
.name,
onProjectSelected: (
selectedProject,
) {
currentProject
.value =
selectedProject;
},
),
) ??
[])
else
Padding(
padding:
const EdgeInsets.all(
16,
),
child: Center(
child:
Text(
'noProjects',
).tr(),
),
),
],
),
),
],
),
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Developer Stats
if (stats != null) ...[
Text(
'Overview',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
loading:
() => const Center(
child: CircularProgressIndicator(),
),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(
currentDeveloper.value?.publisher!.name,
const Gap(16),
_DeveloperStatsWidget(stats: stats),
const Gap(24),
],
// Projects Section
Row(
children: [
Text(
'Projects',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(
color:
Theme.of(context).colorScheme.onSurface,
),
),
const Spacer(),
ElevatedButton.icon(
onPressed: onCreateProject,
icon: const Icon(Symbols.add),
label: const Text('Create Project'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
);
},
),
),
),
],
),
const Gap(16),
// Projects List
projects.value?.isNotEmpty ?? false
? Column(
children:
projects.value!
.map(
(project) => _ProjectListTile(
project: project,
publisherName:
currentDeveloper!
.publisher!
.name,
onProjectSelected:
onProjectSelected,
),
)
.toList(),
)
: Container(
padding: const EdgeInsets.all(48),
alignment: Alignment.center,
child: Text(
'No projects available',
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
fontSize: 16,
),
),
),
],
),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper?.publisher?.name),
);
},
),
),
);
}
@@ -660,10 +785,22 @@ class _ProjectListTile extends HookConsumerWidget {
],
onSelected: (value) {
if (value == 'edit') {
context.pushNamed(
'developerProjectEdit',
pathParameters: {'name': publisherName, 'id': project.id},
);
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'editProject'.tr(),
child: ProjectForm(
publisherName: publisherName,
project: project,
),
),
).then((value) {
if (value != null) {
ref.invalidate(devProjectsProvider(publisherName));
}
});
} else if (value == 'delete') {
showConfirmAlert(
'deleteProjectHint'.tr(),
@@ -817,6 +954,129 @@ class _DeveloperUnselectedWidget extends HookConsumerWidget {
}
}
class ProjectForm extends HookConsumerWidget {
final String publisherName;
final DevProject? project;
const ProjectForm({super.key, required this.publisherName, this.project});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isEditing = project != null;
final formKey = useMemoized(() => GlobalKey<FormState>());
final nameController = useTextEditingController(text: project?.name ?? '');
final slugController = useTextEditingController(text: project?.slug ?? '');
final descriptionController = useTextEditingController(
text: project?.description ?? '',
);
final submitting = useState(false);
Future<void> submit() async {
if (!(formKey.currentState?.validate() ?? false)) return;
try {
submitting.value = true;
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
};
final resp =
isEditing
? await client.put(
'/develop/developers/$publisherName/projects/${project!.id}',
data: data,
)
: await client.post(
'/develop/developers/$publisherName/projects',
data: data,
);
if (!context.mounted) return;
Navigator.of(context).pop(DevProject.fromJson(resp.data));
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return Column(
children: [
Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 16,
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
helperText: 'slugHint'.tr(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
),
const Gap(12),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : submit,
icon: const Icon(Symbols.save),
label: Text(isEditing ? 'saveChanges'.tr() : 'create'.tr()),
),
),
],
).padding(horizontal: 24, vertical: 16);
}
}
class _DeveloperEnrollmentSheet extends HookConsumerWidget {
const _DeveloperEnrollmentSheet();

View File

@@ -168,7 +168,7 @@ final developersProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
String _$devProjectsHash() => r'87fdcab47cd7d79ab019a5625617abeb1ffa1f39';
String _$devProjectsHash() => r'715b395bebda785d38691ffee3b88e50b498c91a';
/// See also [devProjects].
@ProviderFor(devProjects)

View File

@@ -74,28 +74,18 @@ class ProjectDetailView extends HookConsumerWidget {
} else {
return Column(
children: [
TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'customApps'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
Container(
color: Theme.of(context).colorScheme.surface,
child: TabBar(
dividerColor: Colors.transparent,
controller: tabController,
tabs: [
Tab(
child: Text('customApps'.tr(), textAlign: TextAlign.center),
),
),
Tab(
child: Text(
'bots'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
Tab(child: Text('bots'.tr(), textAlign: TextAlign.center)),
],
),
),
Expanded(
child: TabBarView(

View File

@@ -490,14 +490,13 @@ class PostDetailScreen extends HookConsumerWidget {
),
if (user.value != null)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: postState
.when(
bottom: 16 + MediaQuery.of(context).padding.bottom,
left: 16,
right: 16,
child:
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 660),
child: postState.when(
data:
(post) => PostQuickReply(
parent: post!,
@@ -509,13 +508,8 @@ class PostDetailScreen extends HookConsumerWidget {
),
loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(),
)
.padding(
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
horizontal: 16,
),
),
).center(),
),
],
);

View File

@@ -784,6 +784,22 @@ class SettingsScreen extends HookConsumerWidget {
},
),
),
// Disable animation settings
ListTile(
minLeadingWidth: 48,
title: Text('settingsDisableAnimation').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.animation),
trailing: Switch(
value: settings.disableAnimation,
onChanged: (value) {
ref
.read(appSettingsNotifierProvider.notifier)
.setDisableAnimation(value);
},
),
),
];
// Desktop-specific settings

View File

@@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -11,6 +12,16 @@ import 'package:material_symbols_icons/symbols.dart';
final currentRouteProvider = StateProvider<String?>((ref) => null);
const kWideScreenRouteStart = 4;
const kTabRoutes = [
'/',
'/chat',
'/realms',
'/account',
'/creators',
'/developers',
];
class TabsScreen extends HookConsumerWidget {
final Widget? child;
const TabsScreen({super.key, this.child});
@@ -32,6 +43,8 @@ class TabsScreen extends HookConsumerWidget {
notificationUnreadCountNotifierProvider,
);
final wideScreen = isWideScreen(context);
final destinations = [
NavigationDestination(
label: 'explore'.tr(),
@@ -50,19 +63,30 @@ class TabsScreen extends HookConsumerWidget {
child: const Icon(Symbols.account_circle),
),
),
if (wideScreen)
NavigationDestination(
label: 'creatorHub'.tr(),
icon: const Icon(Symbols.draw),
),
if (wideScreen)
NavigationDestination(
label: 'developerHub'.tr(),
icon: const Icon(Symbols.code),
),
];
final routes = ['/', '/chat', '/realms', '/account'];
int getCurrentIndex() {
if (currentLocation.startsWith('/chat')) return 1;
if (currentLocation.startsWith('/realms')) return 2;
if (currentLocation.startsWith('/account')) return 3;
return 0; // Default to explore
if (currentLocation == '/') return 0;
final idx = kTabRoutes.indexWhere(
(p) => currentLocation.startsWith(p),
1,
);
final value = math.max(idx, 0);
return math.min(value, destinations.length - 1);
}
void onDestinationSelected(int index) {
context.go(routes[index]);
context.go(kTabRoutes[index]);
}
final currentIndex = getCurrentIndex();

View File

@@ -0,0 +1,40 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/autocomplete_response.dart';
import 'package:island/pods/network.dart';
final autocompleteServiceProvider = Provider<AutocompleteService>((ref) {
final dio = ref.watch(apiClientProvider);
return AutocompleteService(dio);
});
class AutocompleteService {
final Dio _client;
AutocompleteService(this._client);
Future<List<AutocompleteSuggestion>> getSuggestions(
String roomId,
String content,
) async {
final response = await _client.post(
'/sphere/chat/$roomId/autocomplete',
data: {'content': content},
);
final data = response.data as List<dynamic>;
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
}
Future<List<AutocompleteSuggestion>> getGeneralSuggestions(
String content,
) async {
final response = await _client.post(
'/sphere/autocomplete',
data: {'content': content},
);
final data = response.data as List<dynamic>;
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
}
}

View File

@@ -6,6 +6,25 @@ import 'package:island/models/wallet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
const Map<String, Color> kUsernamePlainColors = {
'red': Colors.red,
'blue': Colors.blue,
'green': Colors.green,
'yellow': Colors.yellow,
'purple': Colors.purple,
'orange': Colors.orange,
'pink': Colors.pink,
'cyan': Colors.cyan,
'lime': Colors.lime,
'indigo': Colors.indigo,
'teal': Colors.teal,
'amber': Colors.amber,
'brown': Colors.brown,
'grey': Colors.grey,
'black': Colors.black,
'white': Colors.white,
};
const kVerificationMarkColors = [
Colors.teal,
Colors.blue,
@@ -17,12 +36,175 @@ const kVerificationMarkColors = [
class AccountName extends StatelessWidget {
final SnAccount account;
final TextStyle? style;
const AccountName({super.key, required this.account, this.style});
final bool ignorePermissions;
const AccountName({
super.key,
required this.account,
this.style,
this.ignorePermissions = false,
});
Alignment _parseGradientDirection(String direction) {
switch (direction) {
case 'to right':
return Alignment.centerLeft;
case 'to left':
return Alignment.centerRight;
case 'to bottom':
return Alignment.topCenter;
case 'to top':
return Alignment.bottomCenter;
case 'to bottom right':
return Alignment.topLeft;
case 'to bottom left':
return Alignment.topRight;
case 'to top right':
return Alignment.bottomLeft;
case 'to top left':
return Alignment.bottomRight;
default:
return Alignment.centerLeft;
}
}
Alignment _parseGradientEnd(String direction) {
switch (direction) {
case 'to right':
return Alignment.centerRight;
case 'to left':
return Alignment.centerLeft;
case 'to bottom':
return Alignment.bottomCenter;
case 'to top':
return Alignment.topCenter;
case 'to bottom right':
return Alignment.bottomRight;
case 'to bottom left':
return Alignment.bottomLeft;
case 'to top right':
return Alignment.topRight;
case 'to top left':
return Alignment.topLeft;
default:
return Alignment.centerRight;
}
}
@override
Widget build(BuildContext context) {
var nameStyle = (style ?? TextStyle());
if (account.perkSubscription != null) {
// Apply username color based on membership tier and custom settings
if (account.profile.usernameColor != null) {
final usernameColor = account.profile.usernameColor!;
final tier = account.perkSubscription?.identifier;
// Check tier restrictions
final canUseCustomColor =
ignorePermissions ||
switch (tier) {
'solian.stellar.primary' =>
usernameColor.type == 'plain' &&
kUsernamePlainColors.containsKey(usernameColor.value),
'solian.stellar.nova' => usernameColor.type == 'plain',
'solian.stellar.supernova' => true,
_ => false,
};
if (canUseCustomColor) {
if (usernameColor.type == 'plain') {
// Plain color
Color? color;
if (kUsernamePlainColors.containsKey(usernameColor.value)) {
color = kUsernamePlainColors[usernameColor.value];
} else if (usernameColor.value != null) {
// Try to parse hex color
try {
color = Color(
int.parse(
usernameColor.value!.replaceFirst('#', ''),
radix: 16,
) +
0xFF000000,
);
} catch (_) {
// Invalid hex, ignore
}
}
if (color != null) {
nameStyle = nameStyle.copyWith(color: color);
}
} else if (usernameColor.type == 'gradient' &&
usernameColor.colors != null &&
usernameColor.colors!.isNotEmpty) {
// Gradient - use ShaderMask for text gradient
final colors = <Color>[];
for (final colorStr in usernameColor.colors!) {
Color? color;
if (kUsernamePlainColors.containsKey(colorStr)) {
color = kUsernamePlainColors[colorStr];
} else {
// Try to parse hex color
try {
color = Color(
int.parse(colorStr.replaceFirst('#', ''), radix: 16) +
0xFF000000,
);
} catch (_) {
// Invalid hex, skip
continue;
}
}
if (color != null) {
colors.add(color);
}
}
if (colors.isNotEmpty) {
final gradient = LinearGradient(
colors: colors,
begin: _parseGradientDirection(
usernameColor.direction ?? 'to right',
),
end: _parseGradientEnd(usernameColor.direction ?? 'to right'),
);
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: ShaderMask(
shaderCallback: (bounds) => gradient.createShader(bounds),
child: Text(
account.nick,
style: nameStyle.copyWith(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
if (account.perkSubscription != null)
StellarMembershipMark(membership: account.perkSubscription!),
if (account.profile.verification != null)
VerificationMark(mark: account.profile.verification!),
if (account.automatedId != null)
Tooltip(
message: 'accountAutomated'.tr(),
child: Icon(
Symbols.smart_toy,
size: 16,
color: nameStyle.color,
fill: 1,
),
),
],
);
}
}
}
} else if (account.perkSubscription != null) {
// Default membership colors if no custom color is set
nameStyle = nameStyle.copyWith(
color: (switch (account.perkSubscription!.identifier) {
'solian.stellar.primary' => Colors.blueAccent,

View File

@@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -6,14 +5,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/event_calendar.dart';
import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/account/account_nameplate.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/account/event_calendar.dart';
import 'package:island/widgets/account/fortune_graph.dart';
import 'package:styled_widget/styled_widget.dart';
class EventCalanderScreen extends HookConsumerWidget {
/// A reusable content widget for event calendar that can be used in screens or sheets
/// This widget manages the calendar state and displays the calendar and fortune graph
class EventCalendarContent extends HookConsumerWidget {
/// Username to fetch calendar for, null means current user ('me')
final String name;
const EventCalanderScreen({super.key, required this.name});
/// Whether this is being displayed in a sheet (affects layout)
final bool isSheet;
const EventCalendarContent({
super.key,
required this.name,
this.isSheet = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -45,13 +54,37 @@ class EventCalanderScreen extends HookConsumerWidget {
selectedDay.value = day;
}
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),
),
body: SingleChildScrollView(
if (isSheet) {
// Sheet layout - simplified, no app bar, scrollable content
return SingleChildScrollView(
child: Column(
children: [
// Use the reusable EventCalendarWidget
EventCalendarWidget(
events: events,
initialDate: now,
showEventDetails: true,
onMonthChanged: onMonthChanged,
onDaySelected: onDaySelected,
),
// Add the fortune graph widget
const Divider(height: 1),
FortuneGraphWidget(
events: events,
onPointSelected: onDaySelected,
).padding(horizontal: 8, vertical: 4),
// Show user profile if viewing someone else's calendar
if (name != 'me' && user.value != null)
AccountNameplate(name: name),
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
);
} else {
// Screen layout - with responsive design
return SingleChildScrollView(
child:
MediaQuery.of(context).size.width > 480
? ConstrainedBox(
@@ -111,7 +144,7 @@ class EventCalanderScreen extends HookConsumerWidget {
Gap(MediaQuery.of(context).padding.bottom + 16),
],
),
),
);
);
}
}
}

View File

@@ -76,10 +76,17 @@ class EventDetailsWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty)
Text(getActivityTitle(status.label, status.meta) ?? status.label),
if ((getActivityTitle(status.label, status.meta) ??
status.label)
.isNotEmpty)
Text(
getActivityTitle(status.label, status.meta) ??
status.label,
),
if (getActivitySubtitle(status.meta) != null)
Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8),
Text(
getActivitySubtitle(status.meta)!,
).fontSize(11).opacity(0.8),
Text(
'${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}',
).fontSize(11).opacity(0.8),
@@ -92,7 +99,7 @@ class EventDetailsWidget extends StatelessWidget {
],
),
if (event?.checkInResult == null && (event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
Text('eventCalandarEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
}

View File

@@ -1,9 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/widgets/account/event_calendar_content.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:styled_widget/styled_widget.dart';
/// A widget that displays a graph of fortune levels over time
@@ -24,7 +25,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
/// Callback when a point is selected
final void Function(DateTime)? onPointSelected;
final String? eventCalanderUser;
final String? eventCalandarUser;
final EdgeInsets? margin;
@@ -35,7 +36,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
this.maxWidth = double.infinity,
this.height = 180,
this.onPointSelected,
this.eventCalanderUser,
this.eventCalandarUser,
this.margin,
});
@@ -59,7 +60,7 @@ class FortuneGraphWidget extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('fortuneGraph').tr().fontSize(18).bold(),
if (eventCalanderUser != null)
if (eventCalandarUser != null)
IconButton(
icon: const Icon(Icons.calendar_month, size: 20),
visualDensity: const VisualDensity(
@@ -69,9 +70,17 @@ class FortuneGraphWidget extends HookConsumerWidget {
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
context.pushNamed(
'accountCalendar',
pathParameters: {'name': eventCalanderUser!},
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'eventCalendar'.tr(),
child: EventCalendarContent(
name: eventCalandarUser!,
isSheet: true,
),
),
);
},
),

View File

@@ -0,0 +1,277 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:fl_heatmap/fl_heatmap.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/heatmap.dart';
import '../services/responsive.dart';
/// A reusable heatmap widget for displaying activity data in GitHub-style layout.
/// Shows exactly 365 days (wide screen) or 90 days (non-wide screen) of data ending at the current date.
class ActivityHeatmapWidget extends HookConsumerWidget {
final SnHeatmap heatmap;
const ActivityHeatmapWidget({super.key, required this.heatmap});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedItem = useState<HeatmapItem?>(null);
final now = DateTime.now();
final isWide = isWideScreen(context);
final days = isWide ? 365 : 90;
// Start from exactly the selected days ago
final startDate = now.subtract(Duration(days: days));
// End at current date
final endDate = now;
// Find monday of the week containing start date
final startMonday = startDate.subtract(
Duration(days: startDate.weekday - 1),
);
// Find sunday of the week containing end date
final endSunday = endDate.add(Duration(days: 7 - endDate.weekday));
// Generate weeks to cover the selected date range
final weeks = <DateTime>[];
var current = startMonday;
while (current.isBefore(endSunday) || current.isAtSameMomentAs(endSunday)) {
weeks.add(current);
current = current.add(const Duration(days: 7));
}
// Create data map for all dates in the range
final dataMap = <DateTime, double>{};
for (final week in weeks) {
for (var i = 0; i < 7; i++) {
final date = week.add(Duration(days: i));
// Only include dates within our selected range
if (date.isAfter(startDate.subtract(const Duration(days: 1))) &&
date.isBefore(endDate.add(const Duration(days: 1)))) {
final item = heatmap.items.firstWhere(
(e) =>
e.date.year == date.year &&
e.date.month == date.month &&
e.date.day == date.day,
orElse: () => SnHeatmapItem(date: date, count: 0),
);
dataMap[date] = item.count.toDouble();
}
}
}
// Generate month labels for the top
final monthLabels = <String>[];
final monthPositions = <int>[];
final processedMonths =
<String>{}; // Track processed months to avoid duplicates
for (final week in weeks) {
final monthKey = '${week.year}-${week.month.toString().padLeft(2, '0')}';
// Only process each month once
if (!processedMonths.contains(monthKey)) {
processedMonths.add(monthKey);
// Find which week this month starts in
final firstDayOfMonth = DateTime(week.year, week.month, 1);
final monthStartMonday = firstDayOfMonth.subtract(
Duration(days: firstDayOfMonth.weekday - 1),
);
final monthStartWeekIndex = weeks.indexWhere(
(w) =>
w.year == monthStartMonday.year &&
w.month == monthStartMonday.month &&
w.day == monthStartMonday.day,
);
if (monthStartWeekIndex != -1) {
monthLabels.add(_getMonthAbbreviation(week.month));
monthPositions.add(monthStartWeekIndex);
}
}
}
final heatmapData = HeatmapData(
rows: [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
], // Days of week vertically
columns:
weeks
.map(
(w) =>
'${w.year}-${w.month.toString().padLeft(2, '0')}-${w.day.toString().padLeft(2, '0')}',
)
.toList(), // Weeks horizontally
items: [
for (int day = 0; day < 7; day++) // For each day of week (Mon-Sun)
for (final week in weeks) // For each week
HeatmapItem(
value: dataMap[week.add(Duration(days: day))] ?? 0.0,
unit: heatmap.unit,
xAxisLabel:
'${week.year}-${week.month.toString().padLeft(2, '0')}-${week.day.toString().padLeft(2, '0')}',
yAxisLabel:
day == 0
? 'Mon'
: day == 1
? 'Tue'
: day == 2
? 'Wed'
: day == 3
? 'Thu'
: day == 4
? 'Fri'
: day == 5
? 'Sat'
: 'Sun',
),
],
);
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'activityHeatmap',
style: Theme.of(context).textTheme.titleMedium,
).tr(),
const Gap(8),
// Month labels row
Row(
children: [
const SizedBox(width: 30), // Space for day labels
...monthLabels.asMap().entries.map((entry) {
final month = entry.value;
return Expanded(
child: Container(
alignment: Alignment.center,
child: Text(
month,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
),
);
}),
],
),
const Gap(4),
Heatmap(
heatmapData: heatmapData,
rowsVisible: 7,
showXAxisLabels: false,
onItemSelectedListener: (item) {
selectedItem.value = item;
},
),
const Gap(8),
// Legend
Row(
children: [
if (selectedItem.value != null)
RichText(
text: TextSpan(
children: [
TextSpan(
text: selectedItem.value!.value.toInt().toString(),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontWeight: FontWeight.bold),
),
TextSpan(
text: ' activities on ',
style: Theme.of(context).textTheme.bodySmall,
),
TextSpan(
text: _formatDate(
selectedItem.value!.xAxisLabel ?? '',
),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const Spacer(),
Text(
'Less',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Gap(4),
// Color indicators (light to dark green)
...[
Colors.green.withOpacity(0.2),
Colors.green.withOpacity(0.4),
Colors.green.withOpacity(0.6),
Colors.green.withOpacity(0.8),
Colors.green,
].map(
(color) => Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
),
const Gap(4),
Text(
'More',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
);
}
String _getMonthAbbreviation(int month) {
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return monthNames[month - 1];
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
final monthAbbrev = _getMonthAbbreviation(date.month);
return '$monthAbbrev ${date.day}, ${date.year}';
} catch (e) {
return dateString; // Fallback to original string if parsing fails
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -68,132 +69,154 @@ class WindowScaffold extends HookConsumerWidget {
return null;
}, []);
final pageButtonActions = [
IconButton(
icon: Icon(Symbols.keyboard_arrow_left),
onPressed:
ref.watch(routerProvider).canPop()
? () => ref.read(routerProvider).pop()
: null,
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(Symbols.home),
onPressed: () => ref.read(routerProvider).go('/'),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
final router = ref.watch(routerProvider);
final pageActionsButton = [
if (router.canPop())
IconButton(
icon: Icon(Symbols.close),
onPressed: router.canPop() ? () => router.pop() : null,
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
)
else
IconButton(
icon: Icon(Symbols.home),
onPressed: () => router.go('/'),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
const Gap(8),
];
if (!kIsWeb &&
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
return Material(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand,
children: [
Column(
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Material(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
fit: StackFit.expand,
children: [
DragToMoveArea(
child:
Platform.isMacOS
? Stack(
alignment: Alignment.center,
children: [
Row(
Column(
children: [
DragToMoveArea(
child:
Platform.isMacOS
? Stack(
alignment: Alignment.center,
children: [
if (Platform.isMacOS)
const SizedBox(width: 80),
...pageButtonActions,
if (isWideScreen(context))
Row(
children: [
const Spacer(),
...pageActionsButton,
],
)
else
SizedBox(height: 32),
Text(
'Solar Network',
textAlign: TextAlign.center,
style: TextStyle(
color:
Theme.of(
context,
).colorScheme.onSurface,
),
),
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Image.asset(
Theme.of(context).brightness ==
Brightness.dark
? 'assets/icons/icon-dark.png'
: 'assets/icons/icon.png',
width: 20,
height: 20,
),
const SizedBox(width: 8),
Text(
'Solar Network',
textAlign: TextAlign.start,
),
],
).padding(horizontal: 12, vertical: 5),
),
IconButton(
icon: Icon(Symbols.minimize),
onPressed: () => windowManager.minimize(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(
isMaximized.value
? Symbols.fullscreen_exit
: Symbols.fullscreen,
),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.restore();
} else {
windowManager.maximize();
}
},
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(Symbols.close),
onPressed: () => windowManager.hide(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
],
),
Text(
'Solar Network',
textAlign: TextAlign.center,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
),
),
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Row(
children: [
Image.asset(
Theme.of(context).brightness ==
Brightness.dark
? 'assets/icons/icon-dark.png'
: 'assets/icons/icon.png',
width: 20,
height: 20,
),
const SizedBox(width: 8),
Text(
'Solar Network',
textAlign: TextAlign.start,
),
],
).padding(horizontal: 12, vertical: 5),
),
IconButton(
icon: Icon(Symbols.minimize),
onPressed: () => windowManager.minimize(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(
isMaximized.value
? Symbols.fullscreen_exit
: Symbols.fullscreen,
),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.restore();
} else {
windowManager.maximize();
}
},
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
IconButton(
icon: Icon(Symbols.close),
onPressed: () => windowManager.hide(),
iconSize: 16,
padding: EdgeInsets.all(8),
constraints: BoxConstraints(),
color: Theme.of(context).iconTheme.color,
),
],
),
),
Expanded(child: child),
],
),
Expanded(child: child),
_WebSocketIndicator(),
],
),
_WebSocketIndicator(),
],
),
),
);
}
return Stack(
fit: StackFit.expand,
children: [Positioned.fill(child: child), _WebSocketIndicator()],
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(ref)},
child: Stack(
fit: StackFit.expand,
children: [Positioned.fill(child: child), _WebSocketIndicator()],
),
),
);
}
}
@@ -254,7 +277,7 @@ class AppScaffold extends HookConsumerWidget {
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final Widget? floatingActionButton;
final AppBar? appBar;
final PreferredSizeWidget? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
final bool? isNoBackground;
@@ -300,37 +323,29 @@ class AppScaffold extends HookConsumerWidget {
],
);
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.escape): const PopIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{PopIntent: PopAction(context)},
child: Focus(
focusNode: focusNode,
child: Scaffold(
extendBody: extendBody ?? true,
extendBodyBehindAppBar: true,
backgroundColor:
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body:
noBackground
? content
: AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: TabbedFabLocation(context),
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
),
),
return Focus(
focusNode: focusNode,
child: Scaffold(
extendBody: extendBody ?? true,
extendBodyBehindAppBar: true,
backgroundColor:
noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body:
noBackground
? content
: AppBackground(isRoot: true, child: content),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: TabbedFabLocation(context),
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
),
);
}
@@ -341,14 +356,14 @@ class PopIntent extends Intent {
}
class PopAction extends Action<PopIntent> {
final BuildContext context;
final WidgetRef ref;
PopAction(this.context);
PopAction(this.ref);
@override
void invoke(PopIntent intent) {
if (context.canPop()) {
context.pop();
if (ref.watch(routerProvider).canPop()) {
ref.read(routerProvider).pop();
}
}
}
@@ -364,7 +379,7 @@ class PageBackButton extends StatelessWidget {
final isDesktop =
!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows);
if (isDesktop) return const SizedBox.shrink();
if (isDesktop && isWideScreen(context)) return const SizedBox.shrink();
return IconButton(
onPressed: () {
@@ -377,9 +392,11 @@ class PageBackButton extends StatelessWidget {
},
icon: Icon(
color: color,
(!kIsWeb && (Platform.isMacOS || Platform.isIOS))
? Symbols.arrow_back_ios_new
: Symbols.arrow_back,
context.canPop()
? (!kIsWeb && (Platform.isMacOS || Platform.isIOS))
? Symbols.arrow_back_ios_new
: Symbols.arrow_back
: Symbols.home,
shadows: shadows,
),
);

View File

@@ -3,20 +3,28 @@ import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_typeahead/flutter_typeahead.dart";
import "package:gap/gap.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:image_picker/image_picker.dart";
import "package:island/models/account.dart";
import "package:island/models/autocomplete_response.dart";
import "package:island/models/chat.dart";
import "package:island/models/file.dart";
import "package:island/models/publisher.dart";
import "package:island/models/realm.dart";
import "package:island/models/sticker.dart";
import "package:island/pods/config.dart";
import "package:island/services/autocomplete_service.dart";
import "package:island/services/responsive.dart";
import "package:island/widgets/content/attachment_preview.dart";
import "package:island/widgets/content/cloud_files.dart";
import "package:island/widgets/shared/upload_menu.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:pasteboard/pasteboard.dart";
import "package:styled_widget/styled_widget.dart";
import "package:material_symbols_icons/symbols.dart";
import "package:island/widgets/stickers/picker.dart";
import "package:island/widgets/stickers/sticker_picker.dart";
import "package:island/pods/chat/chat_subscribe.dart";
class ChatInput extends HookConsumerWidget {
@@ -217,86 +225,200 @@ class ChatInput extends HookConsumerWidget {
key: ValueKey('typing-indicator-none'),
),
),
if (attachments.isNotEmpty)
SizedBox(
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress: attachmentProgress['chat-upload']?[idx],
onRequestUpload: () => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove: (delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(32),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 4,
),
child: Row(
children: [
Icon(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 20,
color: Theme.of(context).colorScheme.primary,
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
child: child,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
: messageForwardingTo != null
? 'Forwarding message'
: 'Editing message',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},
child:
attachments.isNotEmpty
? SizedBox(
key: ValueKey('attachments-${attachments.length}'),
height: 180,
child: ListView.separated(
padding: EdgeInsets.symmetric(horizontal: 12),
scrollDirection: Axis.horizontal,
itemCount: attachments.length,
itemBuilder: (context, idx) {
return SizedBox(
width: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress:
attachmentProgress['chat-upload']?[idx],
onRequestUpload:
() => onUploadAttachment(idx),
onDelete: () => onDeleteAttachment(idx),
onUpdate: (value) {
attachments[idx] = value;
onAttachmentsChanged(attachments);
},
onMove:
(delta) => onMoveAttachment(idx, delta),
),
);
},
separatorBuilder: (_, _) => const Gap(8),
),
).padding(vertical: 12)
: const SizedBox.shrink(
key: ValueKey('no-attachments'),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.2),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1.0,
child: child,
),
SizedBox(
width: 28,
height: 28,
child: InkWell(
onTap: onClear,
child: const Icon(Icons.close, size: 20).center(),
),
),
],
),
),
),
);
},
child:
(messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
? Container(
key: ValueKey(
messageReplyingTo?.id ??
messageForwardingTo?.id ??
messageEditingTo?.id ??
'action',
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
margin: const EdgeInsets.only(
left: 8,
right: 8,
top: 8,
bottom: 8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
messageReplyingTo != null
? Symbols.reply
: messageForwardingTo != null
? Symbols.forward
: Symbols.edit,
size: 18,
color:
Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
messageReplyingTo != null
? 'chatReplyingTo'.tr(
args: [
messageReplyingTo
?.sender
.account
.nick ??
'unknown'.tr(),
],
)
: messageForwardingTo != null
? 'chatForwarding'.tr()
: 'chatEditing'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
tooltip: 'clear'.tr(),
),
),
],
),
if (messageReplyingTo != null ||
messageForwardingTo != null ||
messageEditingTo != null)
Padding(
padding: const EdgeInsets.only(
top: 6,
left: 26,
),
child: Text(
(messageReplyingTo ??
messageForwardingTo ??
messageEditingTo)
?.content ??
'chatNoContent'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall!.copyWith(
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
: const SizedBox.shrink(key: ValueKey('no-action')),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -373,37 +495,149 @@ class ChatInput extends HookConsumerWidget {
],
),
Expanded(
child: TextField(
focusNode: inputFocusNode,
child: TypeAheadField<AutocompleteSuggestion>(
controller: messageController,
keyboardType: TextInputType.multiline,
decoration: InputDecoration(
hintMaxLines: 1,
hintText:
(chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr(
args: [
chatRoom.members!
.map((e) => e.account.nick)
.join(', '),
],
)
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: 3,
minLines: 1,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
focusNode: inputFocusNode,
builder: (context, controller, focusNode) {
return TextField(
focusNode: focusNode,
controller: controller,
keyboardType: TextInputType.multiline,
decoration: InputDecoration(
hintMaxLines: 1,
hintText:
(chatRoom.type == 1 && chatRoom.name == null)
? 'chatDirectMessageHint'.tr(
args: [
chatRoom.members!
.map((e) => e.account.nick)
.join(', '),
],
)
: 'chatMessageHint'.tr(
args: [chatRoom.name!],
),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
counterText:
messageController.text.length > 1024
? '${messageController.text.length}/4096'
: null,
),
maxLines: 3,
minLines: 1,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
if (triggerIndex == -1) return [];
final service = ref.read(autocompleteServiceProvider);
try {
return await service.getSuggestions(
chatRoom.id,
pattern,
);
} catch (e) {
return [];
}
},
itemBuilder: (context, suggestion) {
String title = 'unknown'.tr();
Widget leading = Icon(Symbols.help);
switch (suggestion.type) {
case 'user':
final user = SnAccount.fromJson(suggestion.data);
title = user.nick;
leading = ProfilePictureWidget(
file: user.profile.picture,
radius: 18,
);
break;
case 'chatroom':
final chatRoom = SnChatRoom.fromJson(
suggestion.data,
);
title = chatRoom.name ?? 'Chat Room';
leading = ProfilePictureWidget(
file: chatRoom.picture,
radius: 18,
);
break;
case 'realm':
final realm = SnRealm.fromJson(suggestion.data);
title = realm.name;
leading = ProfilePictureWidget(
file: realm.picture,
radius: 18,
);
break;
case 'publisher':
final publisher = SnPublisher.fromJson(
suggestion.data,
);
title = publisher.name;
leading = ProfilePictureWidget(
file: publisher.picture,
radius: 18,
);
break;
case 'sticker':
final sticker = SnSticker.fromJson(suggestion.data);
title = sticker.slug;
leading = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(
fileId: sticker.imageId,
),
),
);
break;
default:
}
return ListTile(
leading: leading,
title: Text(title),
subtitle: Text(suggestion.keyword),
dense: true,
);
},
onSelected: (suggestion) {
final text = messageController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
text.length,
suggestion.keyword,
);
messageController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: triggerIndex + suggestion.keyword.length,
),
);
},
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
debounceDuration: const Duration(milliseconds: 500),
),
),
IconButton(

View File

@@ -25,6 +25,7 @@ import 'package:island/widgets/content/embed/embed_list.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:swipe_to/swipe_to.dart';
import 'package:island/widgets/content/sheet.dart';
class MessageItemAction {
@@ -159,57 +160,79 @@ class MessageItem extends HookConsumerWidget {
return Stack(
clipBehavior: Clip.none,
children: [
InkWell(
mouseCursor: MouseCursor.defer,
focusColor: Colors.transparent,
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
// Jump to related message
if (['messages.update', 'messages.delete'].contains(message.type) &&
message.meta['message_id'] is String &&
message.meta['message_id'] != null) {
onJump(message.meta['message_id']);
SwipeTo(
rightSwipeWidget: Transform.flip(
flipX: true,
child: Icon(Symbols.menu_open),
).padding(left: 16),
leftSwipeWidget: Icon(
isCurrentUser ? Symbols.forward : Symbols.reply,
).padding(right: 16),
onLeftSwipe: (details) {
if (onAction != null) {
if (isCurrentUser) {
onAction!(MessageItemAction.forward);
} else {
onAction!(MessageItemAction.reply);
}
}
},
child: SizedBox(
width: double.infinity,
child: MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: kFlashDuration),
decoration: BoxDecoration(color: flashColor),
child: switch (settings.messageDisplayStyle) {
'compact' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
},
onRightSwipe: (details) => showActionMenu(),
child: InkWell(
mouseCursor: MouseCursor.defer,
focusColor: Colors.transparent,
onLongPress: showActionMenu,
onSecondaryTap: showActionMenu,
onTap: () {
// Jump to related message
if ([
'messages.update',
'messages.delete',
].contains(message.type) &&
message.meta['message_id'] is String &&
message.meta['message_id'] != null) {
onJump(message.meta['message_id']);
}
},
child: SizedBox(
width: double.infinity,
child: MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: AnimatedContainer(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: kFlashDuration),
decoration: BoxDecoration(color: flashColor),
child: switch (settings.messageDisplayStyle) {
'compact' => MessageItemDisplayIRC(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
'column' => MessageItemDisplayDiscord(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
_ => MessageItemDisplayBubble(
message: message,
isCurrentUser: isCurrentUser,
progress: progress,
showAvatar: showAvatar,
onJump: onJump,
translatedText: translatedText.value,
translating: translating.value,
),
},
),
),
),
),
@@ -314,44 +337,47 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
children: [
Icon(
Symbols.article,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
const Gap(6),
Text(
'messageContent'.tr(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
SizedBox(
height: 24,
child: Row(
children: [
Icon(
Symbols.article,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
),
const Spacer(),
if (_shouldShowExpandButton)
IconButton(
visualDensity: VisualDensity.compact,
icon: Icon(
_isExpanded
? Symbols.expand_less
: Symbols.expand_more,
size: 16,
),
onPressed: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
const Gap(6),
Text(
'messageContent'.tr(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary,
),
),
],
const Spacer(),
if (_shouldShowExpandButton)
IconButton(
visualDensity: VisualDensity.compact,
icon: Icon(
_isExpanded
? Symbols.expand_less
: Symbols.expand_more,
size: 16,
),
onPressed: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 24,
),
),
],
),
),
const Gap(8),
// Selectable content
@@ -403,7 +429,7 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
Navigator.pop(context);
},
),
if (widget.isCurrentUser) const Divider(height: 8),
if (widget.isCurrentUser) const Divider(),
_ActionListTile(
leading: Icon(Symbols.reply),
@@ -422,7 +448,7 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
},
),
if (widget.translatableLanguage) const Divider(height: 8),
if (widget.translatableLanguage) const Divider(),
if (widget.translatableLanguage)
_ActionListTile(
leading: Icon(Symbols.translate),
@@ -439,7 +465,7 @@ class _MessageActionSheetState extends State<MessageActionSheet> {
},
),
if (widget.isMobile) const Divider(height: 8),
if (widget.isMobile) const Divider(),
if (widget.isMobile)
_ActionListTile(
leading: Icon(Symbols.copy_all),

View File

@@ -5,7 +5,6 @@ import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
@@ -14,6 +13,8 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/screens/auth/captcha.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/account/event_calendar_content.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:slide_countdown/slide_countdown.dart';
@@ -256,9 +257,17 @@ class CheckInWidget extends HookConsumerWidget {
if (todayResult.valueOrNull == null) {
checkIn();
} else {
context.pushNamed(
'accountCalendar',
pathParameters: {'name': 'me'},
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'eventCalendar'.tr(),
child: EventCalendarContent(
name: 'me',
isSheet: true,
),
),
);
}
},

View File

@@ -15,12 +15,15 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown_latex.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown_widget/markdown_widget.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'image.dart';
class MarkdownTextContent extends HookConsumerWidget {
static const String stickerRegex = r':([-\w]*\+[-\w]*):';
final String content;
final bool isAutoWarp;
final TextScaler? textScaler;
@@ -47,7 +50,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final baseUrl = ref.watch(serverUrlProvider);
final doesEnlargeSticker = useMemoized(() {
// Check if content only contains one sticker by matching the sticker pattern
final stickerPattern = RegExp(r':([-\w]+):');
final stickerPattern = RegExp(stickerRegex);
final matches = stickerPattern.allMatches(content);
// Content should only contain one sticker and nothing else (except whitespace)
@@ -60,6 +63,31 @@ class MarkdownTextContent extends HookConsumerWidget {
final config =
isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig;
final onMentionTap = useCallback((String type, String id) {
final fullPath = '/$type/$id';
context.push(fullPath);
}, [context]);
final mentionGenerator = MentionChipGenerator(
backgroundColor: Theme.of(context).colorScheme.secondary,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
onTap: onMentionTap,
);
final highlightGenerator = HighlightGenerator(
highlightColor: Theme.of(context).colorScheme.primaryContainer,
);
final spoilerRevealed = useState(false);
final spoilerGenerator = SpoilerGenerator(
backgroundColor: Theme.of(context).colorScheme.tertiary,
foregroundColor: Theme.of(context).colorScheme.onTertiary,
outlineColor: Theme.of(context).colorScheme.outline,
revealed: spoilerRevealed.value,
onToggle: () => spoilerRevealed.value = !spoilerRevealed.value,
);
return MarkdownBlock(
data: content,
selectable: isSelectable,
@@ -96,16 +124,15 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href);
if (url != null) {
if (url.scheme == 'solian') {
if (url.host == 'account') {
context.pushNamed(
'accountProfile',
pathParameters: {'name': url.pathSegments[0]},
);
}
final fullPath = ['/', url.host, url.path].join('');
context.push(fullPath);
return;
}
final whitelistDomains = ['solian.app', 'solsynth.dev'];
if (whitelistDomains.contains(url.host)) {
if (whitelistDomains.any(
(domain) =>
url.host == domain || url.host.endsWith('.$domain'),
)) {
launchUrl(url, mode: LaunchMode.externalApplication);
return;
}
@@ -201,6 +228,7 @@ class MarkdownTextContent extends HookConsumerWidget {
generator: MarkdownTextContent.buildGenerator(
isDark: isDark,
linesMargin: linesMargin,
generators: [mentionGenerator, highlightGenerator, spoilerGenerator],
),
);
}
@@ -208,11 +236,14 @@ class MarkdownTextContent extends HookConsumerWidget {
static MarkdownGenerator buildGenerator({
bool isDark = false,
EdgeInsets? linesMargin,
List<dynamic> generators = const [],
}) {
return MarkdownGenerator(
generators: [latexGenerator],
generators: [latexGenerator, ...generators],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_MetionInlineSyntax(),
_HighlightInlineSyntax(),
_SpoilerInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
@@ -221,24 +252,34 @@ class MarkdownTextContent extends HookConsumerWidget {
}
}
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
class _MetionInlineSyntax extends markdown.InlineSyntax {
_MetionInlineSyntax() : super(r'@[-a-zA-Z0-9_./]+');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final alias = match[0]!;
final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull(
'solian://account/${alias.substring(1)}',
);
parser.addNode(anchor);
final parts = alias.substring(1).split('/');
final typeShortcut = parts.length == 1 ? 'u' : parts.first;
final type = switch (typeShortcut) {
'u' => 'accounts',
'r' => 'realms',
'p' => 'publishers',
"c" => 'chat',
_ => '',
};
final element =
markdown.Element('mention-chip', [markdown.Text(alias)])
..attributes['alias'] = alias
..attributes['type'] = type
..attributes['id'] = parts.last;
parser.addNode(element);
return true;
}
}
class _StickerInlineSyntax extends markdown.InlineSyntax {
_StickerInlineSyntax() : super(r':([-\w]+):');
_StickerInlineSyntax() : super(MarkdownTextContent.stickerRegex);
@override
bool onMatch(markdown.InlineParser parser, Match match) {
@@ -250,3 +291,271 @@ class _StickerInlineSyntax extends markdown.InlineSyntax {
return true;
}
}
class _HighlightInlineSyntax extends markdown.InlineSyntax {
_HighlightInlineSyntax() : super(r'==([^=]+)==');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final text = match[1]!;
final element = markdown.Element('highlight', [markdown.Text(text)]);
parser.addNode(element);
return true;
}
}
class _SpoilerInlineSyntax extends markdown.InlineSyntax {
_SpoilerInlineSyntax() : super(r'=!([^!]+)!=');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final text = match[1]!;
final element = markdown.Element('spoiler', [markdown.Text(text)]);
parser.addNode(element);
return true;
}
}
class MentionSpanNodeGenerator {
final Color backgroundColor;
final Color foregroundColor;
final void Function(String type, String id) onTap;
MentionSpanNodeGenerator({
required this.backgroundColor,
required this.foregroundColor,
required this.onTap,
});
SpanNode? call(
String tag,
Map<String, String> attributes,
List<SpanNode> children,
) {
if (tag == 'mention-chip') {
return MentionChipSpanNode(
attributes: attributes,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
onTap: onTap,
);
}
return null;
}
}
class MentionChipGenerator extends SpanNodeGeneratorWithTag {
MentionChipGenerator({
required Color backgroundColor,
required Color foregroundColor,
required void Function(String type, String id) onTap,
}) : super(
tag: 'mention-chip',
generator: (
markdown.Element element,
MarkdownConfig config,
WidgetVisitor visitor,
) {
return MentionChipSpanNode(
attributes: element.attributes,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
onTap: onTap,
);
},
);
}
class MentionChipSpanNode extends SpanNode {
final Map<String, String> attributes;
final Color backgroundColor;
final Color foregroundColor;
final void Function(String type, String id) onTap;
MentionChipSpanNode({
required this.attributes,
required this.backgroundColor,
required this.foregroundColor,
required this.onTap,
});
@override
InlineSpan build() {
final alias = attributes['alias'] ?? '';
final type = attributes['type'] ?? '';
final id = attributes['id'] ?? '';
final parts = alias.substring(1).split('/');
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: InkWell(
onTap: () => onTap(type, id),
borderRadius: BorderRadius.circular(32),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: backgroundColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(32),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(32)),
),
child: Icon(
switch (parts.first.isEmpty ? 'u' : parts.first) {
'c' => Symbols.forum_rounded,
'r' => Symbols.group_rounded,
'u' => Symbols.person_rounded,
'p' => Symbols.edit_rounded,
_ => Symbols.person_rounded,
},
size: 14,
color: foregroundColor,
fill: 1,
).padding(all: 2),
),
Text(
parts.last,
style: TextStyle(
color: backgroundColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}
class HighlightGenerator extends SpanNodeGeneratorWithTag {
HighlightGenerator({required Color highlightColor})
: super(
tag: 'highlight',
generator: (
markdown.Element element,
MarkdownConfig config,
WidgetVisitor visitor,
) {
return HighlightSpanNode(
text: element.textContent,
highlightColor: highlightColor,
);
},
);
}
class HighlightSpanNode extends SpanNode {
final String text;
final Color highlightColor;
HighlightSpanNode({required this.text, required this.highlightColor});
@override
InlineSpan build() {
return TextSpan(
text: text,
style: TextStyle(backgroundColor: highlightColor),
);
}
}
class SpoilerGenerator extends SpanNodeGeneratorWithTag {
SpoilerGenerator({
required Color backgroundColor,
required Color foregroundColor,
required Color outlineColor,
required bool revealed,
required VoidCallback onToggle,
}) : super(
tag: 'spoiler',
generator: (
markdown.Element element,
MarkdownConfig config,
WidgetVisitor visitor,
) {
return SpoilerSpanNode(
text: element.textContent,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
outlineColor: outlineColor,
revealed: revealed,
onToggle: onToggle,
);
},
);
}
class SpoilerSpanNode extends SpanNode {
final String text;
final Color backgroundColor;
final Color foregroundColor;
final Color outlineColor;
final bool revealed;
final VoidCallback onToggle;
SpoilerSpanNode({
required this.text,
required this.backgroundColor,
required this.foregroundColor,
required this.outlineColor,
required this.revealed,
required this.onToggle,
});
@override
InlineSpan build() {
return WidgetSpan(
child: InkWell(
onTap: onToggle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: revealed ? Colors.transparent : backgroundColor,
border: revealed ? Border.all(color: outlineColor, width: 1) : null,
borderRadius: BorderRadius.circular(4),
),
child:
revealed
? Row(
spacing: 6,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.visibility, size: 18).padding(top: 1),
Flexible(child: Text(text)),
],
)
: Row(
spacing: 6,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.visibility_off,
color: foregroundColor,
size: 18,
),
Flexible(
child:
Text(
'spoiler',
style: TextStyle(color: foregroundColor),
).tr(),
),
],
),
),
),
);
}
}

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/services/responsive.dart';
class ConditionalBottomNav extends HookConsumerWidget {
final Widget child;
@@ -17,10 +19,11 @@ class ConditionalBottomNav extends HookConsumerWidget {
return null;
}, [currentLocation]);
// Use the same route logic as TabsScreen for consistency
const mainTabRoutes = ['/', '/chat', '/realms', '/account'];
final shouldShowBottomNav = mainTabRoutes.contains(currentLocation);
final routes = kTabRoutes.sublist(
0,
isWideScreen(context) ? null : kWideScreenRouteStart,
);
final shouldShowBottomNav = routes.contains(currentLocation);
return shouldShowBottomNav ? child : const SizedBox.shrink();
}

View File

@@ -152,7 +152,7 @@ class PostComposeCard extends HookConsumerWidget {
originalPost: originalPost,
repliedPost: repliedPost,
forwardedPost: forwardedPost,
onSuccess: (SnPost createdPost) {
onSuccess: () {
// Mark as submitted
submitted.value = true;
@@ -165,7 +165,7 @@ class PostComposeCard extends HookConsumerWidget {
ComposeStateUtils.resetForm(state);
// Call the widget's onSubmit callback to trigger activity list refresh
onSubmit?.call(createdPost);
// Note: onSubmit still receives the post from the return value
},
);
}

View File

@@ -50,7 +50,11 @@ class PostComposeDialog extends HookConsumerWidget {
final isWide = isWideScreen(context);
useEffect(() {
if (!prompted.value && originalPost == null && drafts.isNotEmpty) {
if (!prompted.value &&
originalPost == null &&
initialState?.replyingTo == null &&
initialState?.forwardingTo == null &&
drafts.isNotEmpty) {
prompted.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_showRestoreDialog(ref, restoredInitialState);

View File

@@ -1,11 +1,20 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/account.dart';
import 'package:island/models/autocomplete_response.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/models/sticker.dart';
import 'package:island/services/autocomplete_service.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_shared.dart';
/// A reusable widget for the form fields in compose screens.
/// Includes title, description, and content text fields.
class ComposeFormFields extends StatelessWidget {
class ComposeFormFields extends HookConsumerWidget {
final ComposeState state;
final bool enabled;
final bool showPublisherAvatar;
@@ -20,7 +29,7 @@ class ComposeFormFields extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Row(
@@ -111,22 +120,122 @@ class ComposeFormFields extends StatelessWidget {
),
// Content field
TextField(
TypeAheadField<AutocompleteSuggestion>(
controller: state.contentController,
enabled: enabled && state.currentPublisher.value != null,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
builder: (context, controller, focusNode) {
return TextField(
focusNode: focusNode,
controller: controller,
enabled: enabled && state.currentPublisher.value != null,
style: theme.textTheme.bodyMedium,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'postContent'.tr(),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
);
},
suggestionsCallback: (pattern) async {
// Only trigger on @ or :
final atIndex = pattern.lastIndexOf('@');
final colonIndex = pattern.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
if (triggerIndex == -1) return [];
final service = ref.read(autocompleteServiceProvider);
try {
return await service.getGeneralSuggestions(pattern);
} catch (e) {
return [];
}
},
itemBuilder: (context, suggestion) {
String title = 'unknown'.tr();
Widget leading = Icon(Icons.help);
switch (suggestion.type) {
case 'user':
final user = SnAccount.fromJson(suggestion.data);
title = user.nick;
leading = ProfilePictureWidget(
file: user.profile.picture,
radius: 18,
);
break;
case 'chatroom':
final chatRoom = SnChatRoom.fromJson(suggestion.data);
title = chatRoom.name ?? 'Chat Room';
leading = ProfilePictureWidget(
file: chatRoom.picture,
radius: 18,
);
break;
case 'realm':
final realm = SnRealm.fromJson(suggestion.data);
title = realm.name;
leading = ProfilePictureWidget(
file: realm.picture,
radius: 18,
);
break;
case 'publisher':
final publisher = SnPublisher.fromJson(suggestion.data);
title = publisher.name;
leading = ProfilePictureWidget(
file: publisher.picture,
radius: 18,
);
break;
case 'sticker':
final sticker = SnSticker.fromJson(suggestion.data);
title = sticker.slug;
leading = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(fileId: sticker.imageId),
),
);
break;
default:
}
return ListTile(
leading: leading,
title: Text(title),
subtitle: Text(suggestion.keyword),
dense: true,
);
},
onSelected: (suggestion) {
final text = state.contentController.text;
final atIndex = text.lastIndexOf('@');
final colonIndex = text.lastIndexOf(':');
final triggerIndex =
atIndex > colonIndex ? atIndex : colonIndex;
if (triggerIndex == -1) return;
final newText = text.replaceRange(
triggerIndex,
text.length,
suggestion.keyword,
);
state.contentController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: triggerIndex + suggestion.keyword.length,
),
);
},
direction: VerticalDirection.down,
hideOnEmpty: true,
hideOnLoading: true,
debounceDuration: const Duration(milliseconds: 500),
),
],
),

View File

@@ -2,9 +2,11 @@ import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.dart';
@@ -132,6 +134,26 @@ class ComposeSettingsSheet extends HookConsumerWidget {
const ComposeSettingsSheet({super.key, required this.state});
Future<List<SnPostTag>> _fetchTagSuggestions(
String query,
WidgetRef ref,
) async {
if (query.isEmpty) return [];
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'query': query},
);
return response.data
.map<SnPostTag>((json) => SnPostTag.fromJson(json))
.toList();
} catch (e) {
return [];
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
@@ -140,6 +162,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
// Listen to visibility changes to trigger rebuilds
final currentVisibility = useValueListenable(state.visibility);
final currentCategories = useValueListenable(state.categories);
final currentTags = useValueListenable(state.tags);
final currentRealm = useValueListenable(state.realm);
final postCategories = ref.watch(postCategoriesProvider);
final userRealms = ref.watch(realmsJoinedProvider);
@@ -255,23 +278,118 @@ class ComposeSettingsSheet extends HookConsumerWidget {
),
// Tags field
TextFieldTags(
textfieldTagsController: state.tagsController,
textSeparators: const [' ', ','],
letterCase: LetterCase.normal,
validator: (String tag) {
if (tag.isEmpty) {
return 'No, cannot be empty';
}
return null;
},
inputFieldBuilder: (context, inputFieldValues) {
return ChipTagInputField(
inputFieldValues: inputFieldValues,
labelText: 'tags',
hintText: 'tagsHint',
);
},
Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [
Text(
'tags'.tr(),
style: Theme.of(context).textTheme.labelLarge,
),
// Existing tags display
if (currentTags.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children:
currentTags.map((tag) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'#$tag',
style: TextStyle(
color:
Theme.of(
context,
).colorScheme.onPrimary,
fontSize: 14,
),
),
const Gap(4),
InkWell(
onTap: () {
final newTags = List<String>.from(
state.tags.value,
)..remove(tag);
state.tags.value = newTags;
},
child: Icon(
Icons.close,
size: 16,
color:
Theme.of(
context,
).colorScheme.onPrimary,
),
),
],
),
);
}).toList(),
),
// Tag input with autocomplete
TypeAheadField<SnPostTag>(
builder: (context, controller, focusNode) {
return TextField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
hintText: 'addTag'.tr(),
border: InputBorder.none,
isCollapsed: true,
contentPadding: EdgeInsets.zero,
),
onSubmitted: (value) {
state.tags.value = [...state.tags.value, value];
controller.clear();
},
);
},
suggestionsCallback:
(pattern) => _fetchTagSuggestions(pattern, ref),
itemBuilder: (context, suggestion) {
return ListTile(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
title: Text('#${suggestion.slug}'),
dense: true,
);
},
onSelected: (suggestion) {
if (!state.tags.value.contains(suggestion.slug)) {
state.tags.value = [
...state.tags.value,
suggestion.slug,
];
}
},
direction: VerticalDirection.down,
hideOnEmpty: true,
hideOnLoading: true,
debounceDuration: const Duration(milliseconds: 300),
),
],
),
),
// Categories field
@@ -399,6 +517,25 @@ class ComposeSettingsSheet extends HookConsumerWidget {
],
).padding(left: 16, right: 8),
),
// Include current realm if it's not null and not in joined realms
if (currentRealm != null &&
!(userRealms.value ?? []).any(
(r) => r.id == currentRealm.id,
))
DropdownMenuItem<SnRealm?>(
value: currentRealm,
child: Row(
children: [
ProfilePictureWidget(
fileId: currentRealm.picture?.id,
fallbackIcon: Symbols.workspaces,
radius: 16,
),
const SizedBox(width: 12),
Text(currentRealm.name),
],
).padding(left: 16, right: 8),
),
if (userRealms.hasValue)
...(userRealms.value ?? []).map(
(realm) => DropdownMenuItem<SnRealm?>(

View File

@@ -23,7 +23,6 @@ import 'package:island/widgets/post/compose_poll.dart';
import 'package:island/widgets/post/compose_recorder.dart';
import 'package:island/pods/file_pool.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:textfield_tags/textfield_tags.dart';
import 'package:island/talker.dart';
class ComposeState {
@@ -37,7 +36,7 @@ class ComposeState {
final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories;
StringTagController tagsController;
final ValueNotifier<List<String>> tags;
final ValueNotifier<SnRealm?> realm;
final ValueNotifier<SnPostEmbedView?> embedView;
final String draftId;
@@ -56,7 +55,7 @@ class ComposeState {
required this.attachmentProgress,
required this.currentPublisher,
required this.submitting,
required this.tagsController,
required this.tags,
required this.categories,
required this.realm,
required this.embedView,
@@ -90,14 +89,10 @@ class ComposeLogic {
int postType = 0,
}) {
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
final tagsController = StringTagController();
// Initialize tags from original post
if (originalPost != null) {
for (var tag in originalPost.tags) {
tagsController.addTag(tag.slug);
}
}
final tags =
originalPost?.tags.map((tag) => tag.slug).toList() ?? <String>[];
// Initialize categories from original post
final categories = originalPost?.categories ?? <SnPostCategory>[];
@@ -129,7 +124,7 @@ class ComposeLogic {
submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
tagsController: tagsController,
tags: ValueNotifier<List<String>>(tags),
categories: ValueNotifier<List<SnPostCategory>>(categories),
realm: ValueNotifier(originalPost?.realm),
embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView),
@@ -141,10 +136,7 @@ class ComposeLogic {
}
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
final tagsController = StringTagController();
for (var x in draft.tags) {
tagsController.addTag(x.slug);
}
final tags = draft.tags.map((tag) => tag.slug).toList();
return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>(
@@ -158,7 +150,7 @@ class ComposeLogic {
submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null),
tagsController: tagsController,
tags: ValueNotifier<List<String>>(tags),
categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
realm: ValueNotifier(draft.realm),
embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView),
@@ -685,7 +677,7 @@ class ComposeLogic {
'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
'tags': state.tagsController.getTags,
'tags': state.tags.value,
'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value,
@@ -781,7 +773,7 @@ class ComposeLogic {
state.submitting.dispose();
state.attachmentProgress.dispose();
state.currentPublisher.dispose();
state.tagsController.dispose();
state.tags.dispose();
state.categories.dispose();
state.realm.dispose();
state.embedView.dispose();

View File

@@ -173,7 +173,7 @@ class ComposeStateUtils {
state.attachmentProgress.value = {};
// Clear tags
state.tagsController.clearTags();
state.tags.value = [];
// Clear categories
state.categories.value = [];

View File

@@ -16,7 +16,7 @@ class ComposeSubmitUtils {
SnPost? originalPost,
SnPost? repliedPost,
SnPost? forwardedPost,
required Function(SnPost) onSuccess,
required Function() onSuccess,
}) async {
if (state.submitting.value) {
throw Exception('Already submitting');
@@ -75,7 +75,7 @@ class ComposeSubmitUtils {
'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
'tags': state.tagsController.getTags,
'tags': state.tags.value,
'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value,
@@ -94,8 +94,8 @@ class ComposeSubmitUtils {
// Parse the response into a SnPost
final post = SnPost.fromJson(response.data);
// Call the success callback with the created/updated post
onSuccess(post);
// Call the success callback
onSuccess();
return post;
} catch (err) {

View File

@@ -17,11 +17,13 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/content/image.dart';
import 'package:island/widgets/post/post_item_screenshot.dart';
import 'package:island/widgets/post/post_award_sheet.dart';
import 'package:island/widgets/post/post_pin_sheet.dart';
import 'package:island/widgets/post/post_shared.dart';
import 'package:island/widgets/post/embed_view_renderer.dart';
import 'package:island/widgets/post/post_reaction_sheet.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_sheet.dart';
import 'package:island/widgets/post/compose_dialog.dart';
@@ -548,15 +550,39 @@ class PostItem extends HookConsumerWidget {
).colorScheme.primary.withOpacity(0.75),
textColor:
Theme.of(context).colorScheme.onPrimary,
child: _buildReactionIcon(
mostReaction,
32,
).padding(
bottom:
_getReactionImageAvailable(mostReaction)
? 2
: 0,
),
child:
mostReaction.contains('+')
? HookConsumer(
builder: (context, ref, child) {
final baseUrl = ref.watch(
serverUrlProvider,
);
final stickerUri =
'$baseUrl/sphere/stickers/lookup/$mostReaction/open';
return SizedBox(
width: 32,
height: 32,
child:
UniversalImage(
uri: stickerUri,
width: 28,
height: 28,
fit: BoxFit.contain,
).center(),
);
},
)
: _buildReactionIcon(
mostReaction,
32,
).padding(
bottom:
_getReactionImageAvailable(
mostReaction,
)
? 2
: 0,
),
),
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
@@ -572,12 +598,13 @@ class PostItem extends HookConsumerWidget {
context: context,
useRootNavigator: true,
builder: (BuildContext context) {
return _PostReactionSheet(
return PostReactionSheet(
reactionsCount: item.reactionsCount,
reactionsMade: item.reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
postId: item.id,
);
},
);
@@ -679,12 +706,13 @@ class PostReactionList extends HookConsumerWidget {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return _PostReactionSheet(
return PostReactionSheet(
reactionsCount: reactions,
reactionsMade: reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
postId: parentId,
);
},
);
@@ -723,212 +751,3 @@ class PostReactionList extends HookConsumerWidget {
);
}
}
class _PostReactionSheet extends StatelessWidget {
final Map<String, int> reactionsCount;
final Map<String, bool> reactionsMade;
final Function(String symbol, int attitude) onReact;
const _PostReactionSheet({
required this.reactionsCount,
required this.reactionsMade,
required this.onReact,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 20,
right: 16,
bottom: 12,
),
child: Row(
children: [
Text(
'reactions'.plural(
reactionsCount.isNotEmpty
? reactionsCount.values.reduce((a, b) => a + b)
: 0,
),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView(
children: [
_buildReactionSection(
context,
Symbols.mood,
'reactionPositive'.tr(),
0,
),
_buildReactionSection(
context,
Symbols.sentiment_neutral,
'reactionNeutral'.tr(),
1,
),
_buildReactionSection(
context,
Symbols.mood_bad,
'reactionNegative'.tr(),
2,
),
],
),
),
],
);
}
Widget _buildReactionSection(
BuildContext context,
IconData icon,
String title,
int attitude,
) {
final allReactions =
kReactionTemplates.entries
.where((entry) => entry.value.attitude == attitude)
.map((entry) => entry.key)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [Icon(icon), Text(title).fontSize(17).bold()],
).padding(horizontal: 24, top: 16, bottom: 6),
SizedBox(
height: 120,
child: GridView.builder(
scrollDirection: Axis.horizontal,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisExtent: 120,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
childAspectRatio: 1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: allReactions.length,
itemBuilder: (context, index) {
final symbol = allReactions[index];
final count = reactionsCount[symbol] ?? 0;
final hasImage = _getReactionImageAvailable(symbol);
return Badge(
label: Text('x$count'),
isLabelVisible: count > 0,
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 0),
child: Card(
margin: const EdgeInsets.symmetric(vertical: 4),
color: Theme.of(context).colorScheme.surfaceContainerLowest,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
onReact(symbol, attitude);
Navigator.pop(context);
},
child: Container(
decoration:
hasImage
? BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage(
'assets/images/stickers/$symbol.png',
),
fit: BoxFit.cover,
colorFilter:
(reactionsMade[symbol] ?? false)
? ColorFilter.mode(
Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.7),
BlendMode.srcATop,
)
: null,
),
)
: null,
child: Stack(
fit: StackFit.expand,
children: [
if (hasImage)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.7),
Colors.transparent,
],
stops: [0.0, 0.3],
),
),
),
Column(
mainAxisAlignment:
hasImage
? MainAxisAlignment.end
: MainAxisAlignment.center,
children: [
if (!hasImage) _buildReactionIcon(symbol, 36),
Text(
ReactInfo.getTranslationKey(symbol),
textAlign: TextAlign.center,
style: TextStyle(
color: hasImage ? Colors.white : null,
shadows:
hasImage
? [
const Shadow(
blurRadius: 4,
offset: Offset(0.5, 0.5),
color: Colors.black,
),
]
: null,
),
).tr(),
if (hasImage) const Gap(4),
],
),
],
),
),
),
),
);
},
),
),
],
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
@@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers_form.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_dialog.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -69,79 +70,83 @@ class PostQuickReply extends HookConsumerWidget {
return publishers.when(
data:
(data) => Row(
spacing: 8,
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: currentPublisher.value?.picture?.id,
radius: 16,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) currentPublisher.value = value;
});
},
).padding(right: 4),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
(data) => Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
child: ProfilePictureWidget(
fileId: currentPublisher.value?.picture?.id,
radius: 16,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => PublisherModal(),
).then((value) {
if (value is SnPublisher) {
currentPublisher.value = value;
}
});
},
).padding(right: 12),
Expanded(
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: 'postReplyPlaceholder'.tr(),
border: InputBorder.none,
isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 9,
),
),
style: TextStyle(fontSize: 14),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
style: TextStyle(fontSize: 14),
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
IconButton(
onPressed: () {
onLaunch?.call();
GoRouter.of(context)
.pushNamed(
'postCompose',
extra: PostComposeInitialState(
const Gap(8),
IconButton(
onPressed: () async {
onLaunch?.call();
final value = await PostComposeDialog.show(
context,
initialState: PostComposeInitialState(
content: contentController.text,
replyingTo: parent,
),
)
.then((value) {
if (value != null) onPosted?.call();
});
},
icon: const Icon(Symbols.launch, size: 20),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(),
);
if (value != null) onPosted?.call();
},
icon: const Icon(Symbols.launch, size: 20),
visualDensity: VisualDensity.compact,
),
IconButton(
icon:
submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
visualDensity: VisualDensity.compact,
),
],
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
icon:
submitting.value
? SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 3),
)
: Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction,
constraints: const BoxConstraints(),
),
],
),
),
loading: () => const SizedBox.shrink(),
error: (e, _) => const SizedBox.shrink(),

View File

@@ -0,0 +1,698 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/stickers/sticker_picker.dart';
import 'package:island/pods/config.dart';
part 'post_reaction_sheet.g.dart';
@riverpod
class ReactionListNotifier extends _$ReactionListNotifier
with CursorPagingNotifierMixin<SnPostReaction> {
static const int _pageSize = 20;
int? totalCount;
@override
Future<CursorPagingData<SnPostReaction>> build({
required String symbol,
required String postId,
}) {
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnPostReaction>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/$postId/reactions',
queryParameters: {'symbol': symbol, 'offset': offset, 'take': _pageSize},
);
totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
final List<dynamic> data = response.data;
final reactions =
data.map((json) => SnPostReaction.fromJson(json)).toList();
final hasMore = reactions.length == _pageSize;
final nextCursor = hasMore ? (offset + reactions.length).toString() : null;
return CursorPagingData(
items: reactions,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
const kAvailableStickers = {
'angry',
'clap',
'confuse',
'pray',
'thumb_up',
'party',
};
bool _getReactionImageAvailable(String symbol) {
return kAvailableStickers.contains(symbol);
}
Widget _buildReactionIcon(String symbol, double size, {double iconSize = 24}) {
if (_getReactionImageAvailable(symbol)) {
return Image.asset(
'assets/images/stickers/$symbol.png',
width: size,
height: size,
fit: BoxFit.contain,
alignment: Alignment.bottomCenter,
);
} else {
return Text(
kReactionTemplates[symbol]?.icon ?? '',
style: TextStyle(fontSize: iconSize),
);
}
}
class PostReactionSheet extends StatelessWidget {
final Map<String, int> reactionsCount;
final Map<String, bool> reactionsMade;
final Function(String symbol, int attitude) onReact;
final String postId;
const PostReactionSheet({
super.key,
required this.reactionsCount,
required this.reactionsMade,
required this.onReact,
required this.postId,
});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 16,
left: 20,
right: 16,
bottom: 12,
),
child: Row(
children: [
Text(
'reactions'.plural(
reactionsCount.isNotEmpty
? reactionsCount.values.reduce((a, b) => a + b)
: 0,
),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
TabBar(tabs: [Tab(text: 'overview'.tr()), Tab(text: 'custom'.tr())]),
const Divider(height: 1),
Expanded(
child: TabBarView(
children: [
ListView(
children: [
_buildCustomReactionSection(context),
_buildReactionSection(
context,
Symbols.mood,
'reactionPositive'.tr(),
0,
),
_buildReactionSection(
context,
Symbols.sentiment_neutral,
'reactionNeutral'.tr(),
1,
),
_buildReactionSection(
context,
Symbols.mood_bad,
'reactionNegative'.tr(),
2,
),
const Gap(8),
],
),
CustomReactionForm(
postId: postId,
onReact: (s, a) => onReact(s.replaceAll(':', ''), a),
),
],
),
),
],
),
);
}
Widget _buildCustomReactionSection(BuildContext context) {
final customReactions =
reactionsCount.entries
.where((entry) => entry.key.contains('+'))
.map((entry) => entry.key)
.toList();
if (customReactions.isEmpty) return const SizedBox.shrink();
return HookConsumer(
builder: (context, ref, child) {
final baseUrl = ref.watch(serverUrlProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
const Icon(Symbols.emoji_symbols),
Text('customReactions'.tr()).fontSize(17).bold(),
],
).padding(horizontal: 24, top: 16, bottom: 6),
SizedBox(
height: 120,
child: GridView.builder(
scrollDirection: Axis.horizontal,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisExtent: 120,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
childAspectRatio: 1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: customReactions.length,
itemBuilder: (context, index) {
final symbol = customReactions[index];
final count = reactionsCount[symbol] ?? 0;
final stickerUri =
'$baseUrl/sphere/stickers/lookup/$symbol/open';
return GestureDetector(
onLongPressStart: (details) {
if (count > 0) {
showReactionDetailsPopup(
context,
symbol,
details.localPosition,
postId,
reactionsCount[symbol] ?? 0,
);
}
},
onSecondaryTapUp: (details) {
if (count > 0) {
showReactionDetailsPopup(
context,
symbol,
details.localPosition,
postId,
reactionsCount[symbol] ?? 0,
);
}
},
child: Badge(
label: Text('x$count'),
isLabelVisible: count > 0,
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 0),
child: Card(
margin: const EdgeInsets.symmetric(vertical: 4),
color:
Theme.of(
context,
).colorScheme.surfaceContainerLowest,
child: InkWell(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
onTap: () {
onReact(
symbol,
1,
); // Custom reactions use neutral attitude
Navigator.pop(context);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: double.infinity),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(stickerUri),
fit: BoxFit.contain,
colorFilter:
(reactionsMade[symbol] ?? false)
? ColorFilter.mode(
Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.7),
BlendMode.srcATop,
)
: null,
),
),
),
const Gap(8),
Text(
symbol,
style: const TextStyle(
fontSize: 10,
color: Colors.white,
shadows: [
Shadow(
blurRadius: 4,
offset: Offset(0.5, 0.5),
color: Colors.black,
),
],
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
);
},
),
),
],
);
},
);
}
Widget _buildReactionSection(
BuildContext context,
IconData icon,
String title,
int attitude,
) {
final allReactions =
kReactionTemplates.entries
.where((entry) => entry.value.attitude == attitude)
.map((entry) => entry.key)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [Icon(icon), Text(title).fontSize(17).bold()],
).padding(horizontal: 24, top: 16, bottom: 6),
SizedBox(
height: 120,
child: GridView.builder(
scrollDirection: Axis.horizontal,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
mainAxisExtent: 120,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
childAspectRatio: 1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: allReactions.length,
itemBuilder: (context, index) {
final symbol = allReactions[index];
final count = reactionsCount[symbol] ?? 0;
final hasImage = _getReactionImageAvailable(symbol);
return GestureDetector(
onLongPressStart: (details) {
if (count > 0) {
showReactionDetailsPopup(
context,
symbol,
details.localPosition,
postId,
reactionsCount[symbol] ?? 0,
);
}
},
onSecondaryTapUp: (details) {
if (count > 0) {
showReactionDetailsPopup(
context,
symbol,
details.localPosition,
postId,
reactionsCount[symbol] ?? 0,
);
}
},
child: Badge(
label: Text('x$count'),
isLabelVisible: count > 0,
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 0),
child: Card(
margin: const EdgeInsets.symmetric(vertical: 4),
color: Theme.of(context).colorScheme.surfaceContainerLowest,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
onReact(symbol, attitude);
Navigator.pop(context);
},
child: Container(
decoration:
hasImage
? BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: AssetImage(
'assets/images/stickers/$symbol.png',
),
fit: BoxFit.cover,
colorFilter:
(reactionsMade[symbol] ?? false)
? ColorFilter.mode(
Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.7),
BlendMode.srcATop,
)
: null,
),
)
: null,
child: Stack(
fit: StackFit.expand,
children: [
if (hasImage)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.7),
Colors.transparent,
],
stops: [0.0, 0.3],
),
),
),
Column(
mainAxisAlignment:
hasImage
? MainAxisAlignment.end
: MainAxisAlignment.center,
children: [
if (!hasImage) _buildReactionIcon(symbol, 36),
Text(
ReactInfo.getTranslationKey(symbol),
textAlign: TextAlign.center,
style: TextStyle(
color: hasImage ? Colors.white : null,
shadows:
hasImage
? [
const Shadow(
blurRadius: 4,
offset: Offset(0.5, 0.5),
color: Colors.black,
),
]
: null,
),
).tr(),
if (hasImage) const Gap(4),
],
),
],
),
),
),
),
),
);
},
),
),
],
);
}
}
class ReactionDetailsPopup extends HookConsumerWidget {
final String symbol;
final String postId;
final int totalCount;
const ReactionDetailsPopup({
super.key,
required this.symbol,
required this.postId,
required this.totalCount,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = reactionListNotifierProvider(
symbol: symbol,
postId: postId,
);
final width = math.min(MediaQuery.of(context).size.width * 0.8, 480.0);
return PopupCard(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
child: SizedBox(
width: width,
height: 400,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildReactionIcon(symbol, 24),
const Gap(8),
Text(
ReactInfo.getTranslationKey(symbol),
style: Theme.of(context).textTheme.titleMedium,
).tr(),
const Spacer(),
Text('reactions'.plural(totalCount)),
],
),
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: provider,
futureRefreshable: provider.future,
notifierRefreshable: provider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final reaction = data.items[index];
return ListTile(
leading: AccountPfcGestureDetector(
uname: reaction.account?.name ?? 'unknown',
child: ProfilePictureWidget(
file: reaction.account?.profile.picture,
),
),
title: Text(reaction.account?.nick ?? 'unknown'.tr()),
subtitle: Text(
'${reaction.createdAt.formatRelative(context)} · ${reaction.createdAt.formatSystem()}',
),
);
},
),
),
),
],
),
),
);
}
}
class CustomReactionForm extends HookConsumerWidget {
final String postId;
final Function(String symbol, int attitude) onReact;
const CustomReactionForm({
super.key,
required this.postId,
required this.onReact,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final attitude = useState<int>(1);
final symbol = useState<String>('');
Future<void> submitCustomReaction() async {
if (symbol.value.isEmpty) return;
onReact(symbol.value, attitude.value);
Navigator.pop(context);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'customReaction'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
const Gap(24),
TextField(
decoration: InputDecoration(
labelText: 'stickerPlaceholder'.tr(),
hintText: 'prefix+slug',
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
suffixIcon: InkWell(
onTapDown: (details) async {
await showStickerPickerPopover(
context,
details.globalPosition.translate(-300, -280),
alignment: Alignment.topLeft,
onPick: (placeholder) {
// Remove the surrounding : from the placeholder
symbol.value = placeholder.substring(
1,
placeholder.length - 1,
);
},
);
},
child: const Icon(Symbols.sticky_note_2),
),
),
controller: TextEditingController(text: symbol.value),
onChanged: (value) => symbol.value = value,
),
const Gap(24),
Text(
'reactionAttitude'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const Gap(8),
SegmentedButton(
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Symbols.sentiment_satisfied),
label: Text('attitudePositive'.tr()),
),
ButtonSegment(
value: 1,
icon: const Icon(Symbols.sentiment_stressed),
label: Text('attitudeNeutral'.tr()),
),
ButtonSegment(
value: 2,
icon: const Icon(Symbols.sentiment_sad),
label: Text('attitudeNegative'.tr()),
),
],
selected: {attitude.value},
onSelectionChanged: (Set<int> newSelection) {
attitude.value = newSelection.first;
},
),
const Gap(32),
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed: symbol.value.isEmpty ? null : submitCustomReaction,
icon: const Icon(Symbols.send),
label: Text('addReaction'.tr()),
),
),
Gap(MediaQuery.of(context).padding.bottom + 24),
],
),
);
}
}
Future<void> showReactionDetailsPopup(
BuildContext context,
String symbol,
Offset offset,
String postId,
int totalCount,
) async {
await showPopupCard<void>(
offset: offset,
context: context,
builder:
(context) => ReactionDetailsPopup(
symbol: symbol,
postId: postId,
totalCount: totalCount,
),
alignment: Alignment.center,
dimBackground: true,
);
}

View File

@@ -0,0 +1,206 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_reaction_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$reactionListNotifierHash() =>
r'92cf80d2461e46ca62cf6e6a37f8b16c239e7449';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$ReactionListNotifier
extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPostReaction>> {
late final String symbol;
late final String postId;
FutureOr<CursorPagingData<SnPostReaction>> build({
required String symbol,
required String postId,
});
}
/// See also [ReactionListNotifier].
@ProviderFor(ReactionListNotifier)
const reactionListNotifierProvider = ReactionListNotifierFamily();
/// See also [ReactionListNotifier].
class ReactionListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPostReaction>>> {
/// See also [ReactionListNotifier].
const ReactionListNotifierFamily();
/// See also [ReactionListNotifier].
ReactionListNotifierProvider call({
required String symbol,
required String postId,
}) {
return ReactionListNotifierProvider(symbol: symbol, postId: postId);
}
@override
ReactionListNotifierProvider getProviderOverride(
covariant ReactionListNotifierProvider provider,
) {
return call(symbol: provider.symbol, postId: provider.postId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'reactionListNotifierProvider';
}
/// See also [ReactionListNotifier].
class ReactionListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
> {
/// See also [ReactionListNotifier].
ReactionListNotifierProvider({required String symbol, required String postId})
: this._internal(
() =>
ReactionListNotifier()
..symbol = symbol
..postId = postId,
from: reactionListNotifierProvider,
name: r'reactionListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$reactionListNotifierHash,
dependencies: ReactionListNotifierFamily._dependencies,
allTransitiveDependencies:
ReactionListNotifierFamily._allTransitiveDependencies,
symbol: symbol,
postId: postId,
);
ReactionListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.symbol,
required this.postId,
}) : super.internal();
final String symbol;
final String postId;
@override
FutureOr<CursorPagingData<SnPostReaction>> runNotifierBuild(
covariant ReactionListNotifier notifier,
) {
return notifier.build(symbol: symbol, postId: postId);
}
@override
Override overrideWith(ReactionListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ReactionListNotifierProvider._internal(
() =>
create()
..symbol = symbol
..postId = postId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
symbol: symbol,
postId: postId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
>
createElement() {
return _ReactionListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ReactionListNotifierProvider &&
other.symbol == symbol &&
other.postId == postId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, symbol.hashCode);
hash = _SystemHash.combine(hash, postId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ReactionListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPostReaction>> {
/// The parameter `symbol` of this provider.
String get symbol;
/// The parameter `postId` of this provider.
String get postId;
}
class _ReactionListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
>
with ReactionListNotifierRef {
_ReactionListNotifierProviderElement(super.provider);
@override
String get symbol => (origin as ReactionListNotifierProvider).symbol;
@override
String get postId => (origin as ReactionListNotifierProvider).postId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -36,22 +36,18 @@ class PostRepliesSheet extends HookConsumerWidget {
),
// Quick reply section
if (user.value != null)
Material(
elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(post.id));
},
onLaunch: () {
Navigator.of(context).pop();
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 8,
top: 8,
horizontal: 16,
),
PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(post.id));
},
onLaunch: () {
Navigator.of(context).pop();
},
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 8,
horizontal: 16,
),
],
),

View File

@@ -14,7 +14,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
part 'picker.g.dart';
part 'sticker_picker.g.dart';
/// Fetch user-added sticker packs (with stickers) from API:
/// GET /sphere/stickers/me
@@ -34,7 +34,7 @@ Future<List<SnStickerPack>> myStickerPacks(Ref ref) async {
/// Sticker Picker popover dialog
/// - Displays user-owned sticker packs as tabs (chips)
/// - Shows grid of stickers in selected pack
/// - On tap, returns placeholder string :{prefix}{slug}: via onPick callback
/// - On tap, returns placeholder string :{prefix}+{slug}: via onPick callback
class StickerPicker extends HookConsumerWidget {
final void Function(String placeholder) onPick;
@@ -63,7 +63,7 @@ class StickerPicker extends HookConsumerWidget {
return _PackSwitcher(
packs: packs,
onPick: (pack, sticker) {
final placeholder = ':${pack.prefix}${sticker.slug}:';
final placeholder = ':${pack.prefix}+${sticker.slug}:';
HapticFeedback.selectionClick();
onPick(placeholder);
if (Navigator.of(context).canPop()) {
@@ -247,7 +247,7 @@ class _StickersGrid extends StatelessWidget {
itemCount: stickers.length,
itemBuilder: (context, index) {
final sticker = stickers[index];
final placeholder = ':${pack.prefix}${sticker.slug}:';
final placeholder = ':${pack.prefix}+${sticker.slug}:';
return Tooltip(
message: placeholder,
child: InkWell(
@@ -282,13 +282,14 @@ class _StickersGrid extends StatelessWidget {
Future<void> showStickerPickerPopover(
BuildContext context,
Offset offset, {
Alignment? alignment,
required void Function(String placeholder) onPick,
}) async {
// Use flutter_popup_card to present the anchored popup near trigger.
await showPopupCard<void>(
context: context,
offset: offset,
alignment: Alignment.topLeft,
alignment: alignment ?? Alignment.topLeft,
dimBackground: true,
builder:
(ctx) => SizedBox(

View File

@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'picker.dart';
part of 'sticker_picker.dart';
// **************************************************************************
// RiverpodGenerator

View File

@@ -457,6 +457,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.9.0"
dio_smart_retry:
dependency: "direct main"
description:
name: dio_smart_retry
sha256: c8e20da5f49289fa7dce5c9c6b5b120928e3661aefa0fa2d206ea6d93f580928
url: "https://pub.dev"
source: hosted
version: "7.0.1"
dio_web_adapter:
dependency: transitive
description:
@@ -721,6 +729,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_heatmap:
dependency: "direct main"
description:
name: fl_heatmap
sha256: "4f6849588d7733909fffc6f4a4c900ea8f54e0fbd50bc935e24357ac2fe1464d"
url: "https://pub.dev"
source: hosted
version: "0.4.5"
flutter:
dependency: "direct main"
description: flutter

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 3.2.0+134
version: 3.3.0+135
environment:
sdk: ^3.7.2
@@ -161,6 +161,8 @@ dependencies:
app_links: ^6.4.1
syncfusion_flutter_pdfviewer: ^31.1.21
swipe_to: ^1.0.6
fl_heatmap: ^0.4.5
dio_smart_retry: ^7.0.1
dev_dependencies:
flutter_test: