Compare commits

..

10 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
21 changed files with 678 additions and 274 deletions

View File

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

View File

@@ -171,8 +171,8 @@
"checkInResultLevel3": "Good Luck", "checkInResultLevel3": "Good Luck",
"checkInResultLevel4": "Best Luck", "checkInResultLevel4": "Best Luck",
"checkInActivityTitle": "{} checked in on {} and got a {}", "checkInActivityTitle": "{} checked in on {} and got a {}",
"eventCalander": "Event Calander", "eventCalendar": "Event Calendar",
"eventCalanderEmpty": "No events on that day.", "eventCalendarEmpty": "No events on that day.",
"fortuneGraph": "Fortune Trend", "fortuneGraph": "Fortune Trend",
"noFortuneData": "No fortune data available for this month.", "noFortuneData": "No fortune data available for this month.",
"creatorHub": "Creator Hub", "creatorHub": "Creator Hub",
@@ -639,6 +639,10 @@
"chatNotJoined": "You have not joined this chat yet.", "chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.", "chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat", "chatJoin": "Join the Chat",
"chatReplyingTo": "Replying to {}",
"chatForwarding": "Forwarding message",
"chatEditing": "Editing message",
"chatNoContent": "No content",
"realmJoin": "Join the Realm", "realmJoin": "Join the Realm",
"realmJoinSuccess": "Successfully joined the realm.", "realmJoinSuccess": "Successfully joined the realm.",
"search": "Search", "search": "Search",
@@ -1246,5 +1250,7 @@
"addColor": "Add Color", "addColor": "Add Color",
"preview": "Preview", "preview": "Preview",
"availableWithYourPlan": "Available with your plan", "availableWithYourPlan": "Available with your plan",
"upgradeRequired": "Upgrade required" "upgradeRequired": "Upgrade required",
"settingsDisableAnimation": "Disable Animation",
"addTag": "Add Tag"
} }

View File

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

View File

@@ -290,7 +290,7 @@ mixin _$AppSettings {
ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms
double get windowOpacity;// The window opacity for desktop platforms double get windowOpacity;// The window opacity for desktop platforms
double get cardTransparency;// The card background opacity 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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -301,16 +301,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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 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?,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,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 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, 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) { switch (_that) {
case _AppSettings() when $default != null: 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(); 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) { switch (_that) {
case _AppSettings(): 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` /// 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) { switch (_that) {
case _AppSettings() when $default != null: 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; return null;
} }
@@ -504,7 +505,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
class _AppSettings implements AppSettings { 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; @override final bool autoTranslate;
@@ -528,6 +529,7 @@ class _AppSettings implements AppSettings {
@override final String messageDisplayStyle; @override final String messageDisplayStyle;
@override final String? themeMode; @override final String? themeMode;
@override final bool useMaterial3; @override final bool useMaterial3;
@override final bool disableAnimation;
/// Create a copy of AppSettings /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -539,16 +541,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of AppSettings
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_AppSettings(
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable 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 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?,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,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 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, as bool,
)); ));
} }

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -109,6 +110,16 @@ final apiClientProvider = Provider<Dio>((ref) {
printResponseData: false, 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; 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/realms.dart';
import 'package:island/screens/realm/realm_form.dart'; import 'package:island/screens/realm/realm_form.dart';
import 'package:island/screens/realm/realm_detail.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/discovery/realms.dart';
import 'package:island/screens/reports/report_detail.dart'; import 'package:island/screens/reports/report_detail.dart';
import 'package:island/screens/reports/report_list.dart'; import 'package:island/screens/reports/report_list.dart';
@@ -138,14 +137,6 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/logs', path: '/logs',
builder: (context, state) => TalkerScreen(talker: talker), 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);
},
),
// Web articles // Web articles
GoRoute( GoRoute(

View File

@@ -97,9 +97,7 @@ class _AccountBasicInfo extends StatelessWidget {
IconButton( IconButton(
onPressed: () { onPressed: () {
SharePlus.instance.share( SharePlus.instance.share(
ShareParams( ShareParams(uri: Uri.parse('https://solian.app/@${data.name}')),
uri: Uri.parse('https://solian.app/@${data.name}'),
),
); );
}, },
icon: const Icon(Symbols.share), icon: const Icon(Symbols.share),
@@ -879,7 +877,7 @@ class AccountProfileScreen extends HookConsumerWidget {
child: Card( child: Card(
child: FortuneGraphWidget( child: FortuneGraphWidget(
events: accountEvents, events: accountEvents,
eventCalanderUser: data.name, eventCalandarUser: data.name,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
), ),
), ),
@@ -1004,7 +1002,7 @@ class AccountProfileScreen extends HookConsumerWidget {
child: Card( child: Card(
child: FortuneGraphWidget( child: FortuneGraphWidget(
events: accountEvents, events: accountEvents,
eventCalanderUser: data.name, eventCalandarUser: data.name,
), ),
).padding(horizontal: 4), ).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/chat/messages_notifier.dart";
import "package:island/pods/network.dart"; import "package:island/pods/network.dart";
import "package:island/pods/chat/chat_online_count.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/screens/chat/search_messages.dart";
import "package:island/services/file_uploader.dart"; import "package:island/services/file_uploader.dart";
import "package:island/screens/chat/chat.dart"; import "package:island/screens/chat/chat.dart";
@@ -44,6 +45,7 @@ class ChatRoomScreen extends HookConsumerWidget {
final chatIdentity = ref.watch(chatroomIdentityProvider(id)); final chatIdentity = ref.watch(chatroomIdentityProvider(id));
final isSyncing = ref.watch(isSyncingProvider); final isSyncing = ref.watch(isSyncingProvider);
final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id)); final onlineCount = ref.watch(chatOnlineCountNotifierProvider(id));
final settings = ref.watch(appSettingsNotifierProvider);
final hasOnlineCount = onlineCount.hasValue; final hasOnlineCount = onlineCount.hasValue;
@@ -535,7 +537,7 @@ class ChatRoomScreen extends HookConsumerWidget {
listController: listController, listController: listController,
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 16, top: 16,
bottom: 80 + MediaQuery.of(context).padding.bottom, bottom: MediaQuery.of(context).padding.bottom + 16,
), ),
controller: scrollController, controller: scrollController,
reverse: true, // Show newest messages at the bottom reverse: true, // Show newest messages at the bottom
@@ -543,7 +545,9 @@ class ChatRoomScreen extends HookConsumerWidget {
findChildIndexCallback: (key) { findChildIndexCallback: (key) {
if (key is! ValueKey<String>) return null; if (key is! ValueKey<String>) return null;
final messageId = key.value.substring(messageKeyPrefix.length); 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 null for invalid indices to let SuperListView handle it properly
return index >= 0 ? index : null; return index >= 0 ? index : null;
}, },
@@ -561,13 +565,14 @@ class ChatRoomScreen extends HookConsumerWidget {
.abs() > .abs() >
3; 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, skipError: true,
data: data:
(identity) => MessageItem( (identity) => MessageItem(
key: key, key: settings.disableAnimation ? key : null,
message: message, message: message,
isCurrentUser: identity?.id == message.senderId, isCurrentUser: identity?.id == message.senderId,
onAction: (action) { onAction: (action) {
@@ -605,7 +610,6 @@ class ChatRoomScreen extends HookConsumerWidget {
), ),
loading: loading:
() => MessageItem( () => MessageItem(
key: key,
message: message, message: message,
isCurrentUser: false, isCurrentUser: false,
onAction: null, onAction: null,
@@ -613,8 +617,29 @@ class ChatRoomScreen extends HookConsumerWidget {
showAvatar: false, showAvatar: false,
onJump: (_) {}, 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( body: Stack(
children: [ children: [
// Messages // Messages and Input in Column
Positioned.fill( Positioned.fill(
child: messages.when( child: Column(
data: children: [
(messageList) => Expanded(
messageList.isEmpty child: AnimatedSwitcher(
? Center(child: Text('No messages yet'.tr())) duration: const Duration(milliseconds: 300),
: chatMessageListWidget(messageList), switchInCurve: Curves.easeOutCubic,
loading: () => const Center(child: CircularProgressIndicator()), switchOutCurve: Curves.easeInCubic,
error: transitionBuilder: (
(error, _) => ResponseErrorWidget( Widget child,
error: error, Animation<double> animation,
onRetry: () => messagesNotifier.loadInitial(), ) {
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(),
),
),
), ),
), ),
), chatRoom.when(
// Input data:
Positioned( (room) => Column(
bottom: 0, mainAxisSize: MainAxisSize.min,
left: 0, children: [
right: 0, ChatInput(
child: chatRoom.when( messageController: messageController,
data: chatRoom: room!,
(room) => Column( onSend: sendMessage,
mainAxisSize: MainAxisSize.min, onClear: () {
children: [ if (messageEditingTo.value != null) {
ChatInput( attachments.value.clear();
messageController: messageController, messageController.clear();
chatRoom: room!, }
onSend: sendMessage, messageEditingTo.value = null;
onClear: () { messageReplyingTo.value = null;
if (messageEditingTo.value != null) { messageForwardingTo.value = null;
attachments.value.clear(); },
messageController.clear(); messageEditingTo: messageEditingTo.value,
} messageReplyingTo: messageReplyingTo.value,
messageEditingTo.value = null; messageForwardingTo: messageForwardingTo.value,
messageReplyingTo.value = null; onPickFile: (bool isPhoto) {
messageForwardingTo.value = null; if (isPhoto) {
}, pickPhotoMedia();
messageEditingTo: messageEditingTo.value, } else {
messageReplyingTo: messageReplyingTo.value, pickVideoMedia();
messageForwardingTo: messageForwardingTo.value, }
onPickFile: (bool isPhoto) { },
if (isPhoto) { onPickAudio: pickAudioMedia,
pickPhotoMedia(); onPickGeneralFile: pickGeneralFile,
} else { onLinkAttachment: linkAttachment,
pickVideoMedia(); attachments: attachments.value,
} onUploadAttachment: uploadAttachment,
}, onDeleteAttachment: (index) async {
onPickAudio: pickAudioMedia, final attachment = attachments.value[index];
onPickGeneralFile: pickGeneralFile, if (attachment.isOnCloud && !attachment.isLink) {
onLinkAttachment: linkAttachment, final client = ref.watch(apiClientProvider);
attachments: attachments.value, await client.delete(
onUploadAttachment: uploadAttachment, '/drive/files/${attachment.data.id}',
onDeleteAttachment: (index) async { );
final attachment = attachments.value[index]; }
if (attachment.isOnCloud && !attachment.isLink) { final clone = List.of(attachments.value);
final client = ref.watch(apiClientProvider); clone.removeAt(index);
await client.delete( attachments.value = clone;
'/drive/files/${attachment.data.id}', },
); onMoveAttachment: (idx, delta) {
} if (idx + delta < 0 ||
final clone = List.of(attachments.value); idx + delta >= attachments.value.length) {
clone.removeAt(index); return;
attachments.value = clone; }
}, final clone = List.of(attachments.value);
onMoveAttachment: (idx, delta) { clone.insert(idx + delta, clone.removeAt(idx));
if (idx + delta < 0 || attachments.value = clone;
idx + delta >= attachments.value.length) { },
return; onAttachmentsChanged: (newAttachments) {
} attachments.value = newAttachments;
final clone = List.of(attachments.value); },
clone.insert(idx + delta, clone.removeAt(idx)); attachmentProgress: attachmentProgress.value,
attachments.value = clone; ),
}, Gap(MediaQuery.of(context).padding.bottom),
onAttachmentsChanged: (newAttachments) { ],
attachments.value = newAttachments;
},
attachmentProgress: attachmentProgress.value,
), ),
Gap(MediaQuery.of(context).padding.bottom), error: (_, _) => const SizedBox.shrink(),
], loading: () => const SizedBox.shrink(),
), ),
error: (_, _) => const SizedBox.shrink(), ],
loading: () => const SizedBox.shrink(),
), ),
), ),
Positioned( Positioned(

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 // Desktop-specific settings

View File

@@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.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/pods/event_calendar.dart';
import 'package:island/screens/account/profile.dart'; import 'package:island/screens/account/profile.dart';
import 'package:island/widgets/account/account_nameplate.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/event_calendar.dart';
import 'package:island/widgets/account/fortune_graph.dart'; import 'package:island/widgets/account/fortune_graph.dart';
import 'package:styled_widget/styled_widget.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; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -45,13 +54,37 @@ class EventCalanderScreen extends HookConsumerWidget {
selectedDay.value = day; selectedDay.value = day;
} }
return AppScaffold( if (isSheet) {
isNoBackground: false, // Sheet layout - simplified, no app bar, scrollable content
appBar: AppBar( return SingleChildScrollView(
leading: const PageBackButton(), child: Column(
title: Text('eventCalander').tr(), children: [
), // Use the reusable EventCalendarWidget
body: SingleChildScrollView( 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: child:
MediaQuery.of(context).size.width > 480 MediaQuery.of(context).size.width > 480
? ConstrainedBox( ? ConstrainedBox(
@@ -111,7 +144,7 @@ class EventCalanderScreen extends HookConsumerWidget {
Gap(MediaQuery.of(context).padding.bottom + 16), Gap(MediaQuery.of(context).padding.bottom + 16),
], ],
), ),
), );
); }
} }
} }

View File

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

View File

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

View File

@@ -225,86 +225,200 @@ class ChatInput extends HookConsumerWidget {
key: ValueKey('typing-indicator-none'), key: ValueKey('typing-indicator-none'),
), ),
), ),
if (attachments.isNotEmpty) AnimatedSwitcher(
SizedBox( duration: const Duration(milliseconds: 250),
height: 180, switchInCurve: Curves.easeOutCubic,
child: ListView.separated( switchOutCurve: Curves.easeInCubic,
padding: EdgeInsets.symmetric(horizontal: 12), transitionBuilder: (Widget child, Animation<double> animation) {
scrollDirection: Axis.horizontal, return SlideTransition(
itemCount: attachments.length, position: Tween<Offset>(
itemBuilder: (context, idx) { begin: const Offset(0, 0.1),
return SizedBox( end: Offset.zero,
width: 180, ).animate(animation),
child: AttachmentPreview( child: FadeTransition(
isCompact: true, opacity: animation,
item: attachments[idx], child: SizeTransition(
progress: attachmentProgress['chat-upload']?[idx], sizeFactor: animation,
onRequestUpload: () => onUploadAttachment(idx), axisAlignment: -1.0,
onDelete: () => onDeleteAttachment(idx), child: child,
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,
), ),
const Gap(8), ),
Expanded( );
child: Text( },
messageReplyingTo != null child:
? 'Replying to ${messageReplyingTo?.sender.account.nick}' attachments.isNotEmpty
: messageForwardingTo != null ? SizedBox(
? 'Forwarding message' key: ValueKey('attachments-${attachments.length}'),
: 'Editing message', height: 180,
style: Theme.of(context).textTheme.bodySmall, child: ListView.separated(
maxLines: 1, padding: EdgeInsets.symmetric(horizontal: 12),
overflow: TextOverflow.ellipsis, 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( child:
onTap: onClear, (messageReplyingTo != null ||
child: const Icon(Icons.close, size: 20).center(), 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( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

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

View File

@@ -530,7 +530,11 @@ class SpoilerSpanNode extends SpanNode {
? Row( ? Row(
spacing: 6, spacing: 6,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [Icon(Symbols.visibility, size: 18), Text(text)], crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.visibility, size: 18).padding(top: 1),
Flexible(child: Text(text)),
],
) )
: Row( : Row(
spacing: 6, spacing: 6,
@@ -541,7 +545,13 @@ class SpoilerSpanNode extends SpanNode {
color: foregroundColor, color: foregroundColor,
size: 18, size: 18,
), ),
Text(text, style: TextStyle(color: foregroundColor)), Flexible(
child:
Text(
'spoiler',
style: TextStyle(color: foregroundColor),
).tr(),
),
], ],
), ),
), ),

View File

@@ -2,9 +2,11 @@ import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart'; import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
@@ -132,6 +134,26 @@ class ComposeSettingsSheet extends HookConsumerWidget {
const ComposeSettingsSheet({super.key, required this.state}); 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -140,6 +162,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
// Listen to visibility changes to trigger rebuilds // Listen to visibility changes to trigger rebuilds
final currentVisibility = useValueListenable(state.visibility); final currentVisibility = useValueListenable(state.visibility);
final currentCategories = useValueListenable(state.categories); final currentCategories = useValueListenable(state.categories);
final currentTags = useValueListenable(state.tags);
final currentRealm = useValueListenable(state.realm); final currentRealm = useValueListenable(state.realm);
final postCategories = ref.watch(postCategoriesProvider); final postCategories = ref.watch(postCategoriesProvider);
final userRealms = ref.watch(realmsJoinedProvider); final userRealms = ref.watch(realmsJoinedProvider);
@@ -255,23 +278,118 @@ class ComposeSettingsSheet extends HookConsumerWidget {
), ),
// Tags field // Tags field
TextFieldTags( Container(
textfieldTagsController: state.tagsController, decoration: BoxDecoration(
textSeparators: const [' ', ','], border: Border.all(
letterCase: LetterCase.normal, color: Theme.of(context).colorScheme.outline,
validator: (String tag) { width: 1,
if (tag.isEmpty) { ),
return 'No, cannot be empty'; borderRadius: BorderRadius.circular(12),
} ),
return null; padding: const EdgeInsets.all(16),
}, child: Column(
inputFieldBuilder: (context, inputFieldValues) { crossAxisAlignment: CrossAxisAlignment.start,
return ChipTagInputField( spacing: 12,
inputFieldValues: inputFieldValues, children: [
labelText: 'tags', Text(
hintText: 'tagsHint', '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 // Categories field
@@ -399,6 +517,25 @@ class ComposeSettingsSheet extends HookConsumerWidget {
], ],
).padding(left: 16, right: 8), ).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) if (userRealms.hasValue)
...(userRealms.value ?? []).map( ...(userRealms.value ?? []).map(
(realm) => DropdownMenuItem<SnRealm?>( (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/widgets/post/compose_recorder.dart';
import 'package:island/pods/file_pool.dart'; import 'package:island/pods/file_pool.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:textfield_tags/textfield_tags.dart';
import 'package:island/talker.dart'; import 'package:island/talker.dart';
class ComposeState { class ComposeState {
@@ -37,7 +36,7 @@ class ComposeState {
final ValueNotifier<SnPublisher?> currentPublisher; final ValueNotifier<SnPublisher?> currentPublisher;
final ValueNotifier<bool> submitting; final ValueNotifier<bool> submitting;
final ValueNotifier<List<SnPostCategory>> categories; final ValueNotifier<List<SnPostCategory>> categories;
StringTagController tagsController; final ValueNotifier<List<String>> tags;
final ValueNotifier<SnRealm?> realm; final ValueNotifier<SnRealm?> realm;
final ValueNotifier<SnPostEmbedView?> embedView; final ValueNotifier<SnPostEmbedView?> embedView;
final String draftId; final String draftId;
@@ -56,7 +55,7 @@ class ComposeState {
required this.attachmentProgress, required this.attachmentProgress,
required this.currentPublisher, required this.currentPublisher,
required this.submitting, required this.submitting,
required this.tagsController, required this.tags,
required this.categories, required this.categories,
required this.realm, required this.realm,
required this.embedView, required this.embedView,
@@ -90,14 +89,10 @@ class ComposeLogic {
int postType = 0, int postType = 0,
}) { }) {
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString(); final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
final tagsController = StringTagController();
// Initialize tags from original post // Initialize tags from original post
if (originalPost != null) { final tags =
for (var tag in originalPost.tags) { originalPost?.tags.map((tag) => tag.slug).toList() ?? <String>[];
tagsController.addTag(tag.slug);
}
}
// Initialize categories from original post // Initialize categories from original post
final categories = originalPost?.categories ?? <SnPostCategory>[]; final categories = originalPost?.categories ?? <SnPostCategory>[];
@@ -129,7 +124,7 @@ class ComposeLogic {
submitting: ValueNotifier<bool>(false), submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}), attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher), currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
tagsController: tagsController, tags: ValueNotifier<List<String>>(tags),
categories: ValueNotifier<List<SnPostCategory>>(categories), categories: ValueNotifier<List<SnPostCategory>>(categories),
realm: ValueNotifier(originalPost?.realm), realm: ValueNotifier(originalPost?.realm),
embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView), embedView: ValueNotifier<SnPostEmbedView?>(originalPost?.embedView),
@@ -141,10 +136,7 @@ class ComposeLogic {
} }
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) { static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
final tagsController = StringTagController(); final tags = draft.tags.map((tag) => tag.slug).toList();
for (var x in draft.tags) {
tagsController.addTag(x.slug);
}
return ComposeState( return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>( attachments: ValueNotifier<List<UniversalFile>>(
@@ -158,7 +150,7 @@ class ComposeLogic {
submitting: ValueNotifier<bool>(false), submitting: ValueNotifier<bool>(false),
attachmentProgress: ValueNotifier<Map<int, double>>({}), attachmentProgress: ValueNotifier<Map<int, double>>({}),
currentPublisher: ValueNotifier<SnPublisher?>(null), currentPublisher: ValueNotifier<SnPublisher?>(null),
tagsController: tagsController, tags: ValueNotifier<List<String>>(tags),
categories: ValueNotifier<List<SnPostCategory>>(draft.categories), categories: ValueNotifier<List<SnPostCategory>>(draft.categories),
realm: ValueNotifier(draft.realm), realm: ValueNotifier(draft.realm),
embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView), embedView: ValueNotifier<SnPostEmbedView?>(draft.embedView),
@@ -685,7 +677,7 @@ class ComposeLogic {
'type': state.postType, 'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id, if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.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(), 'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.realm.value != null) 'realm_id': state.realm.value?.id, if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value, if (state.pollId.value != null) 'poll_id': state.pollId.value,
@@ -781,7 +773,7 @@ class ComposeLogic {
state.submitting.dispose(); state.submitting.dispose();
state.attachmentProgress.dispose(); state.attachmentProgress.dispose();
state.currentPublisher.dispose(); state.currentPublisher.dispose();
state.tagsController.dispose(); state.tags.dispose();
state.categories.dispose(); state.categories.dispose();
state.realm.dispose(); state.realm.dispose();
state.embedView.dispose(); state.embedView.dispose();

View File

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

View File

@@ -75,7 +75,7 @@ class ComposeSubmitUtils {
'type': state.postType, 'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id, if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.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(), 'categories': state.categories.value.map((e) => e.slug).toList(),
if (state.realm.value != null) 'realm_id': state.realm.value?.id, if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value, if (state.pollId.value != null) 'poll_id': state.pollId.value,

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 # 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 # 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. # 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: environment:
sdk: ^3.7.2 sdk: ^3.7.2