Compare commits

...

24 Commits

Author SHA1 Message Date
387d19d85c 🚀 Launch 3.5.0+163 (SNAPSHOT) 2026-01-16 01:50:27 +08:00
f7b991663f Render post article thumbnail 2026-01-16 01:44:41 +08:00
3a57f4265b Editing article thumbnail 2026-01-16 01:30:46 +08:00
d1ee2e5160 💄 Optimize the notification overlay styling 2026-01-16 00:56:57 +08:00
bcd6753ed2 💄 Updating compose toolbar styling 2026-01-16 00:47:32 +08:00
321ea4458b 💄 Optimize the notification overlays 2026-01-16 00:21:07 +08:00
8ad31dad58 Notification stacks on the mobile 2026-01-16 00:08:56 +08:00
269c17d068 💄 The sfx can be played multiple times 2026-01-15 23:05:39 +08:00
a9abd777e1 💫 Better notification overlay animation 2026-01-15 23:02:21 +08:00
e24b1fc135 💫 Optimize the notification animation 2026-01-15 19:34:03 +08:00
d5feea52fa 💄 Optimize notification item 2026-01-15 19:31:11 +08:00
491252bba9 💫 Optimize the overlay animation of notifications 2026-01-15 01:39:48 +08:00
4f569fbefd ♻️ New notification overlay basis 2026-01-15 01:31:09 +08:00
476da28b5e ♻️ Improved new notifcation style 2026-01-15 01:04:15 +08:00
d639df7623 ♻️ Optimize the sfx playing 2026-01-15 00:44:38 +08:00
e1fc5311d2 🐛 Fix some bugs 2026-01-15 00:09:41 +08:00
d0e4fde6c2 🐛 Added some platform checks 2026-01-14 22:50:06 +08:00
9437339b0f 💄 Give name for pasted image 2026-01-14 22:40:10 +08:00
dd7696132c 👽 Authorized audio, video access 2026-01-14 22:34:17 +08:00
95daa3c28d 💄 Use unlimited variant of the fortune card, close #234 2026-01-14 01:53:24 +08:00
ac5193e1f6 🐛 Fix webfeed issues, close #235 2026-01-14 01:49:27 +08:00
0328a7736a Optimize retry logic 2026-01-14 01:06:34 +08:00
03b332f677 🐛 Adjust analytics service initialization 2026-01-14 00:56:28 +08:00
91b2797fb9 👽 Authorized image load request
💄 Optimize image styling
2026-01-13 23:15:36 +08:00
40 changed files with 2165 additions and 757 deletions

View File

@@ -1592,5 +1592,7 @@
"tasksCount": {
"one": "{} task",
"other": "{} tasks"
}
},
"setAsThumbnail": "Set as thumbnail",
"unsetAsThumbnail": "Unset as thumbnail"
}

View File

@@ -15,6 +15,10 @@
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB042F08D99C008390F3 /* SwiftUI.framework */; };
7301DB102F08D99D008390F3 /* SolianWidgetExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7301DB012F08D99C008390F3 /* SolianWidgetExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73595B1B2F17FF8000AAD53C /* SfxMessage.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B162F17FF8000AAD53C /* SfxMessage.caf */; };
73595B1C2F17FF8000AAD53C /* SfxNotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B172F17FF8000AAD53C /* SfxNotification.caf */; };
73595B832F1803D300AAD53C /* SfxNotification.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B172F17FF8000AAD53C /* SfxNotification.caf */; };
73595B842F1803D300AAD53C /* SfxMessage.caf in Resources */ = {isa = PBXBuildFile; fileRef = 73595B162F17FF8000AAD53C /* SfxMessage.caf */; };
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -133,6 +137,8 @@
7301DB042F08D99C008390F3 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
7301DB162F08D9A5008390F3 /* SolianWidgetExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SolianWidgetExtensionExtension.entitlements; sourceTree = "<group>"; };
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
73595B162F17FF8000AAD53C /* SfxMessage.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SfxMessage.caf; sourceTree = "<group>"; };
73595B172F17FF8000AAD53C /* SfxNotification.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SfxNotification.caf; sourceTree = "<group>"; };
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
@@ -396,6 +402,8 @@
91E124CE95BCB4DCD890160D /* Pods */,
498A09270B73B217F0279168 /* Frameworks */,
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */,
73595B162F17FF8000AAD53C /* SfxMessage.caf */,
73595B172F17FF8000AAD53C /* SfxNotification.caf */,
);
sourceTree = "<group>";
};
@@ -695,6 +703,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
73595B1B2F17FF8000AAD53C /* SfxMessage.caf in Resources */,
73595B1C2F17FF8000AAD53C /* SfxNotification.caf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -703,6 +713,8 @@
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
73595B832F1803D300AAD53C /* SfxNotification.caf in Resources */,
73595B842F1803D300AAD53C /* SfxMessage.caf in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,

View File

@@ -143,7 +143,12 @@ RoomInputManager useRoomInputManager(WidgetRef ref, String roomId) {
final newAttachments = [
...attachments.value,
UniversalFile(
data: XFile.fromData(image, mimeType: "image/jpeg"),
displayName: 'image.jpeg',
data: XFile.fromData(
image,
mimeType: "image/jpeg",
name: 'image.jpeg',
),
type: UniversalFileType.image,
),
];

View File

@@ -12,9 +12,11 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:island/services/analytics_service.dart';
import 'package:island/talker.dart';
import 'package:island/firebase_options.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/audio.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/theme.dart';
import 'package:island/pods/userinfo.dart';
@@ -123,6 +125,14 @@ void main() async {
);
}
try {
talker.info("[Analytics] Initializing Analytics service...");
final analyticsService = AnalyticsService();
analyticsService.initialize();
} catch (err) {
talker.error("[Analytics] Failed to initialize Analytics service... $err");
}
final prefs = await SharedPreferences.getInstance();
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
@@ -196,8 +206,12 @@ void main() async {
runApp(
ProviderScope(
retry: (retryCount, error) {
if (error is DioException && error.response?.statusCode == 404) {
return null;
if (retryCount > 3) return null;
if (error is DioException) {
if (error.response?.statusCode == 401) return null;
if (error.response?.statusCode == 403) return null;
if (error.response?.statusCode == 404) return null;
if (error.response?.statusCode == 500) return null;
}
return const Duration(milliseconds: 300);
},
@@ -326,6 +340,9 @@ class IslandApp extends HookConsumerWidget {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
initializeLocalNotifications();
ref.read(audioSessionProvider);
ref.read(notificationSfxProvider);
ref.read(messageSfxProvider);
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
}

View File

@@ -37,6 +37,45 @@ sealed class UniversalFile with _$UniversalFile {
}
}
@freezed
sealed class SnFileReplica with _$SnFileReplica {
const factory SnFileReplica({
required String id,
required String objectId,
required String poolId,
required SnFilePool? pool,
required String storageId,
required int status,
required bool isPrimary,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnFileReplica;
factory SnFileReplica.fromJson(Map<String, dynamic> json) =>
_$SnFileReplicaFromJson(json);
}
@freezed
sealed class SnCloudFileObject with _$SnCloudFileObject {
const factory SnCloudFileObject({
required String id,
required int size,
required Map<String, dynamic>? meta,
required String? mimeType,
required String? hash,
required bool hasCompression,
required bool hasThumbnail,
required List<SnFileReplica> fileReplicas,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnCloudFileObject;
factory SnCloudFileObject.fromJson(Map<String, dynamic> json) =>
_$SnCloudFileObjectFromJson(json);
}
@freezed
sealed class SnCloudFile with _$SnCloudFile {
const factory SnCloudFile({
@@ -45,13 +84,11 @@ sealed class SnCloudFile with _$SnCloudFile {
required String? description,
required Map<String, dynamic>? fileMeta,
required Map<String, dynamic>? userMeta,
required SnFilePool? pool,
@Default([]) List<int> sensitiveMarks,
required String? mimeType,
required String? hash,
required int size,
required DateTime? uploadedAt,
required String? uploadedTo,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,

View File

@@ -279,42 +279,42 @@ as String?,
/// @nodoc
mixin _$SnCloudFile {
mixin _$SnFileReplica {
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; SnFilePool? get pool; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get url;
/// Create a copy of SnCloudFile
String get id; String get objectId; String get poolId; SnFilePool? get pool; String get storageId; int get status; bool get isPrimary; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCloudFile>(this as SnCloudFile, _$identity);
$SnFileReplicaCopyWith<SnFileReplica> get copyWith => _$SnFileReplicaCopyWithImpl<SnFileReplica>(this as SnFileReplica, _$identity);
/// Serializes this SnCloudFile to a JSON map.
/// Serializes this SnFileReplica to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.url, url) || other.url == url));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFileReplica&&(identical(other.id, id) || other.id == id)&&(identical(other.objectId, objectId) || other.objectId == objectId)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.pool, pool) || other.pool == pool)&&(identical(other.storageId, storageId) || other.storageId == storageId)&&(identical(other.status, status) || other.status == status)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),pool,const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt,url);
int get hashCode => Object.hash(runtimeType,id,objectId,poolId,pool,storageId,status,isPrimary,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url)';
return 'SnFileReplica(id: $id, objectId: $objectId, poolId: $poolId, pool: $pool, storageId: $storageId, status: $status, isPrimary: $isPrimary, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnCloudFileCopyWith<$Res> {
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
abstract mixin class $SnFileReplicaCopyWith<$Res> {
factory $SnFileReplicaCopyWith(SnFileReplica value, $Res Function(SnFileReplica) _then) = _$SnFileReplicaCopyWithImpl;
@useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url
String id, String objectId, String poolId, SnFilePool? pool, String storageId, int status, bool isPrimary, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -322,37 +322,31 @@ $SnFilePoolCopyWith<$Res>? get pool;
}
/// @nodoc
class _$SnCloudFileCopyWithImpl<$Res>
implements $SnCloudFileCopyWith<$Res> {
_$SnCloudFileCopyWithImpl(this._self, this._then);
class _$SnFileReplicaCopyWithImpl<$Res>
implements $SnFileReplicaCopyWith<$Res> {
_$SnFileReplicaCopyWithImpl(this._self, this._then);
final SnCloudFile _self;
final $Res Function(SnCloudFile) _then;
final SnFileReplica _self;
final $Res Function(SnFileReplica) _then;
/// Create a copy of SnCloudFile
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? url = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? objectId = null,Object? poolId = null,Object? pool = freezed,Object? storageId = null,Object? status = null,Object? isPrimary = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as String,objectId: null == objectId ? _self.objectId : objectId // ignore: cast_nullable_to_non_nullable
as String,poolId: null == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
as String,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
as SnFilePool?,storageId: null == storageId ? _self.storageId : storageId // ignore: cast_nullable_to_non_nullable
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,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?,url: freezed == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String?,
as DateTime?,
));
}
/// Create a copy of SnCloudFile
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -368,6 +362,607 @@ $SnFilePoolCopyWith<$Res>? get pool {
}
/// Adds pattern-matching-related methods to [SnFileReplica].
extension SnFileReplicaPatterns on SnFileReplica {
/// 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( _SnFileReplica value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnFileReplica() 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( _SnFileReplica value) $default,){
final _that = this;
switch (_that) {
case _SnFileReplica():
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( _SnFileReplica value)? $default,){
final _that = this;
switch (_that) {
case _SnFileReplica() 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 objectId, String poolId, SnFilePool? pool, String storageId, int status, bool isPrimary, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnFileReplica() when $default != null:
return $default(_that.id,_that.objectId,_that.poolId,_that.pool,_that.storageId,_that.status,_that.isPrimary,_that.createdAt,_that.updatedAt,_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 objectId, String poolId, SnFilePool? pool, String storageId, int status, bool isPrimary, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnFileReplica():
return $default(_that.id,_that.objectId,_that.poolId,_that.pool,_that.storageId,_that.status,_that.isPrimary,_that.createdAt,_that.updatedAt,_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 objectId, String poolId, SnFilePool? pool, String storageId, int status, bool isPrimary, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnFileReplica() when $default != null:
return $default(_that.id,_that.objectId,_that.poolId,_that.pool,_that.storageId,_that.status,_that.isPrimary,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnFileReplica implements SnFileReplica {
const _SnFileReplica({required this.id, required this.objectId, required this.poolId, required this.pool, required this.storageId, required this.status, required this.isPrimary, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnFileReplica.fromJson(Map<String, dynamic> json) => _$SnFileReplicaFromJson(json);
@override final String id;
@override final String objectId;
@override final String poolId;
@override final SnFilePool? pool;
@override final String storageId;
@override final int status;
@override final bool isPrimary;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnFileReplicaCopyWith<_SnFileReplica> get copyWith => __$SnFileReplicaCopyWithImpl<_SnFileReplica>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnFileReplicaToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFileReplica&&(identical(other.id, id) || other.id == id)&&(identical(other.objectId, objectId) || other.objectId == objectId)&&(identical(other.poolId, poolId) || other.poolId == poolId)&&(identical(other.pool, pool) || other.pool == pool)&&(identical(other.storageId, storageId) || other.storageId == storageId)&&(identical(other.status, status) || other.status == status)&&(identical(other.isPrimary, isPrimary) || other.isPrimary == isPrimary)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,objectId,poolId,pool,storageId,status,isPrimary,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnFileReplica(id: $id, objectId: $objectId, poolId: $poolId, pool: $pool, storageId: $storageId, status: $status, isPrimary: $isPrimary, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnFileReplicaCopyWith<$Res> implements $SnFileReplicaCopyWith<$Res> {
factory _$SnFileReplicaCopyWith(_SnFileReplica value, $Res Function(_SnFileReplica) _then) = __$SnFileReplicaCopyWithImpl;
@override @useResult
$Res call({
String id, String objectId, String poolId, SnFilePool? pool, String storageId, int status, bool isPrimary, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnFilePoolCopyWith<$Res>? get pool;
}
/// @nodoc
class __$SnFileReplicaCopyWithImpl<$Res>
implements _$SnFileReplicaCopyWith<$Res> {
__$SnFileReplicaCopyWithImpl(this._self, this._then);
final _SnFileReplica _self;
final $Res Function(_SnFileReplica) _then;
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? objectId = null,Object? poolId = null,Object? pool = freezed,Object? storageId = null,Object? status = null,Object? isPrimary = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnFileReplica(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,objectId: null == objectId ? _self.objectId : objectId // ignore: cast_nullable_to_non_nullable
as String,poolId: null == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
as String,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
as SnFilePool?,storageId: null == storageId ? _self.storageId : storageId // ignore: cast_nullable_to_non_nullable
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
as bool,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnFileReplica
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnFilePoolCopyWith<$Res>? get pool {
if (_self.pool == null) {
return null;
}
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
return _then(_self.copyWith(pool: value));
});
}
}
/// @nodoc
mixin _$SnCloudFileObject {
String get id; int get size; Map<String, dynamic>? get meta; String? get mimeType; String? get hash; bool get hasCompression; bool get hasThumbnail; List<SnFileReplica> get fileReplicas; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnCloudFileObject
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnCloudFileObjectCopyWith<SnCloudFileObject> get copyWith => _$SnCloudFileObjectCopyWithImpl<SnCloudFileObject>(this as SnCloudFileObject, _$identity);
/// Serializes this SnCloudFileObject to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFileObject&&(identical(other.id, id) || other.id == id)&&(identical(other.size, size) || other.size == size)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.hasCompression, hasCompression) || other.hasCompression == hasCompression)&&(identical(other.hasThumbnail, hasThumbnail) || other.hasThumbnail == hasThumbnail)&&const DeepCollectionEquality().equals(other.fileReplicas, fileReplicas)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,size,const DeepCollectionEquality().hash(meta),mimeType,hash,hasCompression,hasThumbnail,const DeepCollectionEquality().hash(fileReplicas),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCloudFileObject(id: $id, size: $size, meta: $meta, mimeType: $mimeType, hash: $hash, hasCompression: $hasCompression, hasThumbnail: $hasThumbnail, fileReplicas: $fileReplicas, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnCloudFileObjectCopyWith<$Res> {
factory $SnCloudFileObjectCopyWith(SnCloudFileObject value, $Res Function(SnCloudFileObject) _then) = _$SnCloudFileObjectCopyWithImpl;
@useResult
$Res call({
String id, int size, Map<String, dynamic>? meta, String? mimeType, String? hash, bool hasCompression, bool hasThumbnail, List<SnFileReplica> fileReplicas, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnCloudFileObjectCopyWithImpl<$Res>
implements $SnCloudFileObjectCopyWith<$Res> {
_$SnCloudFileObjectCopyWithImpl(this._self, this._then);
final SnCloudFileObject _self;
final $Res Function(SnCloudFileObject) _then;
/// Create a copy of SnCloudFileObject
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? size = null,Object? meta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? hasCompression = null,Object? hasThumbnail = null,Object? fileReplicas = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,hasCompression: null == hasCompression ? _self.hasCompression : hasCompression // ignore: cast_nullable_to_non_nullable
as bool,hasThumbnail: null == hasThumbnail ? _self.hasThumbnail : hasThumbnail // ignore: cast_nullable_to_non_nullable
as bool,fileReplicas: null == fileReplicas ? _self.fileReplicas : fileReplicas // ignore: cast_nullable_to_non_nullable
as List<SnFileReplica>,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?,
));
}
}
/// Adds pattern-matching-related methods to [SnCloudFileObject].
extension SnCloudFileObjectPatterns on SnCloudFileObject {
/// 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( _SnCloudFileObject value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnCloudFileObject() 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( _SnCloudFileObject value) $default,){
final _that = this;
switch (_that) {
case _SnCloudFileObject():
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( _SnCloudFileObject value)? $default,){
final _that = this;
switch (_that) {
case _SnCloudFileObject() 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, int size, Map<String, dynamic>? meta, String? mimeType, String? hash, bool hasCompression, bool hasThumbnail, List<SnFileReplica> fileReplicas, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnCloudFileObject() when $default != null:
return $default(_that.id,_that.size,_that.meta,_that.mimeType,_that.hash,_that.hasCompression,_that.hasThumbnail,_that.fileReplicas,_that.createdAt,_that.updatedAt,_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, int size, Map<String, dynamic>? meta, String? mimeType, String? hash, bool hasCompression, bool hasThumbnail, List<SnFileReplica> fileReplicas, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnCloudFileObject():
return $default(_that.id,_that.size,_that.meta,_that.mimeType,_that.hash,_that.hasCompression,_that.hasThumbnail,_that.fileReplicas,_that.createdAt,_that.updatedAt,_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, int size, Map<String, dynamic>? meta, String? mimeType, String? hash, bool hasCompression, bool hasThumbnail, List<SnFileReplica> fileReplicas, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnCloudFileObject() when $default != null:
return $default(_that.id,_that.size,_that.meta,_that.mimeType,_that.hash,_that.hasCompression,_that.hasThumbnail,_that.fileReplicas,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnCloudFileObject implements SnCloudFileObject {
const _SnCloudFileObject({required this.id, required this.size, required final Map<String, dynamic>? meta, required this.mimeType, required this.hash, required this.hasCompression, required this.hasThumbnail, required final List<SnFileReplica> fileReplicas, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_fileReplicas = fileReplicas;
factory _SnCloudFileObject.fromJson(Map<String, dynamic> json) => _$SnCloudFileObjectFromJson(json);
@override final String id;
@override final int size;
final Map<String, dynamic>? _meta;
@override Map<String, dynamic>? get meta {
final value = _meta;
if (value == null) return null;
if (_meta is EqualUnmodifiableMapView) return _meta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override final String? mimeType;
@override final String? hash;
@override final bool hasCompression;
@override final bool hasThumbnail;
final List<SnFileReplica> _fileReplicas;
@override List<SnFileReplica> get fileReplicas {
if (_fileReplicas is EqualUnmodifiableListView) return _fileReplicas;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_fileReplicas);
}
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnCloudFileObject
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnCloudFileObjectCopyWith<_SnCloudFileObject> get copyWith => __$SnCloudFileObjectCopyWithImpl<_SnCloudFileObject>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnCloudFileObjectToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFileObject&&(identical(other.id, id) || other.id == id)&&(identical(other.size, size) || other.size == size)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.hasCompression, hasCompression) || other.hasCompression == hasCompression)&&(identical(other.hasThumbnail, hasThumbnail) || other.hasThumbnail == hasThumbnail)&&const DeepCollectionEquality().equals(other._fileReplicas, _fileReplicas)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,size,const DeepCollectionEquality().hash(_meta),mimeType,hash,hasCompression,hasThumbnail,const DeepCollectionEquality().hash(_fileReplicas),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnCloudFileObject(id: $id, size: $size, meta: $meta, mimeType: $mimeType, hash: $hash, hasCompression: $hasCompression, hasThumbnail: $hasThumbnail, fileReplicas: $fileReplicas, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnCloudFileObjectCopyWith<$Res> implements $SnCloudFileObjectCopyWith<$Res> {
factory _$SnCloudFileObjectCopyWith(_SnCloudFileObject value, $Res Function(_SnCloudFileObject) _then) = __$SnCloudFileObjectCopyWithImpl;
@override @useResult
$Res call({
String id, int size, Map<String, dynamic>? meta, String? mimeType, String? hash, bool hasCompression, bool hasThumbnail, List<SnFileReplica> fileReplicas, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnCloudFileObjectCopyWithImpl<$Res>
implements _$SnCloudFileObjectCopyWith<$Res> {
__$SnCloudFileObjectCopyWithImpl(this._self, this._then);
final _SnCloudFileObject _self;
final $Res Function(_SnCloudFileObject) _then;
/// Create a copy of SnCloudFileObject
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? size = null,Object? meta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? hasCompression = null,Object? hasThumbnail = null,Object? fileReplicas = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnCloudFileObject(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,hasCompression: null == hasCompression ? _self.hasCompression : hasCompression // ignore: cast_nullable_to_non_nullable
as bool,hasThumbnail: null == hasThumbnail ? _self.hasThumbnail : hasThumbnail // ignore: cast_nullable_to_non_nullable
as bool,fileReplicas: null == fileReplicas ? _self._fileReplicas : fileReplicas // ignore: cast_nullable_to_non_nullable
as List<SnFileReplica>,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?,
));
}
}
/// @nodoc
mixin _$SnCloudFile {
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String? get url;
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCloudFile>(this as SnCloudFile, _$identity);
/// Serializes this SnCloudFile to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,createdAt,updatedAt,deletedAt,url);
@override
String toString() {
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url)';
}
}
/// @nodoc
abstract mixin class $SnCloudFileCopyWith<$Res> {
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
@useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url
});
}
/// @nodoc
class _$SnCloudFileCopyWithImpl<$Res>
implements $SnCloudFileCopyWith<$Res> {
_$SnCloudFileCopyWithImpl(this._self, this._then);
final SnCloudFile _self;
final $Res Function(SnCloudFile) _then;
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? url = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,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?,url: freezed == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [SnCloudFile].
extension SnCloudFilePatterns on SnCloudFile {
/// A variant of `map` that fallback to returning `orElse`.
@@ -443,10 +1038,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnCloudFile() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);case _:
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);case _:
return orElse();
}
@@ -464,10 +1059,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url) $default,) {final _that = this;
switch (_that) {
case _SnCloudFile():
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);}
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -481,10 +1076,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url)? $default,) {final _that = this;
switch (_that) {
case _SnCloudFile() when $default != null:
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);case _:
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.url);case _:
return null;
}
@@ -496,7 +1091,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
@JsonSerializable()
class _SnCloudFile implements SnCloudFile {
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, required this.pool, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt, this.url}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.createdAt, required this.updatedAt, required this.deletedAt, this.url}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
@override final String id;
@@ -520,7 +1115,6 @@ class _SnCloudFile implements SnCloudFile {
return EqualUnmodifiableMapView(value);
}
@override final SnFilePool? pool;
final List<int> _sensitiveMarks;
@override@JsonKey() List<int> get sensitiveMarks {
if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks;
@@ -532,7 +1126,6 @@ class _SnCloudFile implements SnCloudFile {
@override final String? hash;
@override final int size;
@override final DateTime? uploadedAt;
@override final String? uploadedTo;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
@@ -551,16 +1144,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.url, url) || other.url == url));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.url, url) || other.url == url));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),pool,const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt,url);
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,createdAt,updatedAt,deletedAt,url);
@override
String toString() {
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url)';
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, url: $url)';
}
@@ -571,11 +1164,11 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String? url
});
@override $SnFilePoolCopyWith<$Res>? get pool;
}
/// @nodoc
@@ -588,21 +1181,19 @@ class __$SnCloudFileCopyWithImpl<$Res>
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? url = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? url = freezed,}) {
return _then(_SnCloudFile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,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?,url: freezed == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
@@ -610,19 +1201,7 @@ as String?,
));
}
/// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnFilePoolCopyWith<$Res>? get pool {
if (_self.pool == null) {
return null;
}
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
return _then(_self.copyWith(pool: value));
});
}
}

View File

@@ -29,15 +29,78 @@ const _$UniversalFileTypeEnumMap = {
UniversalFileType.file: 'file',
};
_SnFileReplica _$SnFileReplicaFromJson(Map<String, dynamic> json) =>
_SnFileReplica(
id: json['id'] as String,
objectId: json['object_id'] as String,
poolId: json['pool_id'] as String,
pool: json['pool'] == null
? null
: SnFilePool.fromJson(json['pool'] as Map<String, dynamic>),
storageId: json['storage_id'] as String,
status: (json['status'] as num).toInt(),
isPrimary: json['is_primary'] as bool,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnFileReplicaToJson(_SnFileReplica instance) =>
<String, dynamic>{
'id': instance.id,
'object_id': instance.objectId,
'pool_id': instance.poolId,
'pool': instance.pool?.toJson(),
'storage_id': instance.storageId,
'status': instance.status,
'is_primary': instance.isPrimary,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnCloudFileObject _$SnCloudFileObjectFromJson(Map<String, dynamic> json) =>
_SnCloudFileObject(
id: json['id'] as String,
size: (json['size'] as num).toInt(),
meta: json['meta'] as Map<String, dynamic>?,
mimeType: json['mime_type'] as String?,
hash: json['hash'] as String?,
hasCompression: json['has_compression'] as bool,
hasThumbnail: json['has_thumbnail'] as bool,
fileReplicas: (json['file_replicas'] as List<dynamic>)
.map((e) => SnFileReplica.fromJson(e as Map<String, dynamic>))
.toList(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnCloudFileObjectToJson(_SnCloudFileObject instance) =>
<String, dynamic>{
'id': instance.id,
'size': instance.size,
'meta': instance.meta,
'mime_type': instance.mimeType,
'hash': instance.hash,
'has_compression': instance.hasCompression,
'has_thumbnail': instance.hasThumbnail,
'file_replicas': instance.fileReplicas.map((e) => e.toJson()).toList(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String?,
fileMeta: json['file_meta'] as Map<String, dynamic>?,
userMeta: json['user_meta'] as Map<String, dynamic>?,
pool: json['pool'] == null
? null
: SnFilePool.fromJson(json['pool'] as Map<String, dynamic>),
sensitiveMarks:
(json['sensitive_marks'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
@@ -49,7 +112,6 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
uploadedAt: json['uploaded_at'] == null
? null
: DateTime.parse(json['uploaded_at'] as String),
uploadedTo: json['uploaded_to'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
@@ -65,13 +127,11 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
'description': instance.description,
'file_meta': instance.fileMeta,
'user_meta': instance.userMeta,
'pool': instance.pool?.toJson(),
'sensitive_marks': instance.sensitiveMarks,
'mime_type': instance.mimeType,
'hash': instance.hash,
'size': instance.size,
'uploaded_at': instance.uploadedAt?.toIso8601String(),
'uploaded_to': instance.uploadedTo,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),

65
lib/pods/audio.dart Normal file
View File

@@ -0,0 +1,65 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:island/pods/config.dart';
import 'package:audio_session/audio_session.dart';
final sfxPlayerProvider = Provider<AudioPlayer>((ref) {
final player = AudioPlayer();
ref.onDispose(() {
player.dispose();
});
return player;
});
Future<void> _configureAudioSession() async {
final session = await AudioSession.instance;
await session.configure(
const AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.mixWithOthers,
),
);
await session.setActive(true);
}
final audioSessionProvider = FutureProvider<void>((ref) async {
await _configureAudioSession();
});
final notificationSfxProvider = FutureProvider<void>((ref) async {
final player = ref.watch(sfxPlayerProvider);
await player.setVolume(0.75);
await player.setAudioSource(
AudioSource.asset('assets/audio/notification.mp3'),
preload: true,
);
});
final messageSfxProvider = FutureProvider<void>((ref) async {
final player = ref.watch(sfxPlayerProvider);
await player.setAudioSource(
AudioSource.asset('assets/audio/messages.mp3'),
preload: true,
);
});
Future<void> _playSfx(String assetPath, double volume) async {
final player = AudioPlayer();
await player.setVolume(volume);
await player.setAudioSource(AudioSource.asset(assetPath));
await player.play();
await player.dispose();
}
void playNotificationSfx(WidgetRef ref) {
final settings = ref.read(appSettingsProvider);
if (!settings.soundEffects) return;
_playSfx('assets/audio/notification.mp3', 0.75);
}
void playMessageSfx(WidgetRef ref) {
final settings = ref.read(appSettingsProvider);
if (!settings.soundEffects) return;
_playSfx('assets/audio/messages.mp3', 0.75);
}

View File

@@ -223,7 +223,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
// Add new typing status
_typingStatuses.add(sender);
}
state = List.of(_typingStatuses);
if (ref.mounted) state = List.of(_typingStatuses);
return;
}
@@ -243,11 +243,14 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
// Play sound for new messages when app is unfocused
if (pkt.type == 'messages.new' &&
message.senderId != _chatIdentity.id &&
ref.read(appLifecycleStateProvider).value != AppLifecycleState.resumed &&
ref.read(appLifecycleStateProvider).value !=
AppLifecycleState.resumed &&
ref.read(appSettingsProvider).soundEffects) {
final player = AudioPlayer();
await player.setVolume(0.75);
await player.setAudioSource(AudioSource.asset('assets/audio/messages.mp3'));
await player.setAudioSource(
AudioSource.asset('assets/audio/messages.mp3'),
);
await player.play();
player.dispose();
}

View File

@@ -59,7 +59,7 @@ final class ChatSubscribeNotifierProvider
}
String _$chatSubscribeNotifierHash() =>
r'b7624ae45ace2944a88f8b4d14ddce556c236371';
r'944cb0c1b1805050470d4b79c60937f622d7b716';
final class ChatSubscribeNotifierFamily extends $Family
with

View File

@@ -0,0 +1,99 @@
import 'dart:async';
import 'package:island/models/account.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:uuid/uuid.dart';
part 'notification.g.dart';
const kNotificationBaseDuration = Duration(seconds: 5);
const kNotificationStackedDuration = Duration(seconds: 1);
class NotificationItem {
final String id;
final SnNotification notification;
final DateTime createdAt;
final int index;
final Duration duration;
final bool dismissed;
NotificationItem({
String? id,
required this.notification,
DateTime? createdAt,
required this.index,
Duration? duration,
this.dismissed = false,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? DateTime.now(),
duration =
duration ?? kNotificationBaseDuration + Duration(seconds: index);
NotificationItem copyWith({
String? id,
SnNotification? notification,
DateTime? createdAt,
int? index,
Duration? duration,
bool? dismissed,
}) {
return NotificationItem(
id: id ?? this.id,
notification: notification ?? this.notification,
createdAt: createdAt ?? this.createdAt,
index: index ?? this.index,
duration: duration ?? this.duration,
dismissed: dismissed ?? this.dismissed,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is NotificationItem && other.id == id;
}
@override
int get hashCode => id.hashCode;
}
@riverpod
class NotificationState extends _$NotificationState {
final Map<String, Timer> _timers = {};
@override
List<NotificationItem> build() {
return [];
}
void add(SnNotification notification, {Duration? duration}) {
final newItem = NotificationItem(
notification: notification,
index: state.length,
duration: duration,
);
state = [...state, newItem];
_timers[newItem.id] = Timer(newItem.duration, () => dismiss(newItem.id));
}
void dismiss(String id) {
_timers[id]?.cancel();
_timers.remove(id);
final index = state.indexWhere((item) => item.id == id);
if (index != -1) {
state = List.from(state)..[index] = state[index].copyWith(dismissed: true);
}
}
void remove(String id) {
state = state.where((item) => item.id != id).toList();
}
void clear() {
for (final timer in _timers.values) {
timer.cancel();
}
_timers.clear();
state = [];
}
}

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(NotificationState)
final notificationStateProvider = NotificationStateProvider._();
final class NotificationStateProvider
extends $NotifierProvider<NotificationState, List<NotificationItem>> {
NotificationStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'notificationStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$notificationStateHash();
@$internal
@override
NotificationState create() => NotificationState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<NotificationItem> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<NotificationItem>>(value),
);
}
}
String _$notificationStateHash() => r'8625e77d28d71237d86f6d06efab437aa7c09df1';
abstract class _$NotificationState extends $Notifier<List<NotificationItem>> {
List<NotificationItem> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref as $Ref<List<NotificationItem>, List<NotificationItem>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<List<NotificationItem>, List<NotificationItem>>,
List<NotificationItem>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View File

@@ -35,7 +35,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/publishers/${arg.pubName}/feeds/${arg.feedId}',
'/insight/publishers/${arg.pubName}/feeds/${arg.feedId}',
);
return SnWebFeed.fromJson(response.data);
} catch (e) {
@@ -47,7 +47,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final url = '/sphere/publishers/${feed.publisherId}/feeds';
final url = '/insight/publishers/${feed.publisherId}/feeds';
final response = feed.id.isEmpty
? await client.post(url, data: feed.toJson())
@@ -67,7 +67,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.delete('/sphere/publishers/${arg.pubName}/feeds/$feedId');
await client.delete('/insight/publishers/${arg.pubName}/feeds/$feedId');
state = AsyncValue.data(
SnWebFeed(
id: '',
@@ -93,7 +93,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/sphere/publishers/${arg.pubName}/feeds/$feedId/scrap',
'/insight/publishers/${arg.pubName}/feeds/$feedId/scrap',
options: Options(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 180),

View File

@@ -105,11 +105,9 @@ class WebfeedForm extends HookConsumerWidget {
return feedAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: (error, _) => ResponseErrorWidget(
error: error,
onRetry:
() => ref.invalidate(
onRetry: () => ref.invalidate(
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
),
),
@@ -140,7 +138,7 @@ class WebfeedForm extends HookConsumerWidget {
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
if (context.mounted) isLoading.value = false;
}
}, [pubName, feedId, ref, context, isLoading]);
@@ -160,8 +158,8 @@ class WebfeedForm extends HookConsumerWidget {
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
@@ -184,8 +182,8 @@ class WebfeedForm extends HookConsumerWidget {
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
@@ -197,8 +195,8 @@ class WebfeedForm extends HookConsumerWidget {
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
maxLines: 3,
),
const SizedBox(height: 24),
@@ -256,7 +254,12 @@ class WebfeedForm extends HookConsumerWidget {
],
).padding(horizontal: 20, vertical: 12);
return Column(children: [Expanded(child: formWidget), buttonsRow]);
return Column(
children: [
Expanded(child: formWidget),
buttonsRow,
],
);
},
);
}

View File

@@ -200,7 +200,7 @@ class _DashboardGridNarrow extends HookConsumerWidget {
if (userInfo.value != null && userInfo.value?.activatedAt == null)
AccountUnactivatedCard(),
CheckInWidget(margin: EdgeInsets.zero),
FortuneCard(),
FortuneCard(unlimited: true),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: PostFeaturedList(),
@@ -304,7 +304,9 @@ class ClockCard extends HookConsumerWidget {
),
],
),
Row(
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
spacing: 5,
children: [
notableDay.when(
@@ -318,6 +320,7 @@ class ClockCard extends HookConsumerWidget {
),
],
),
),
],
),
),
@@ -528,13 +531,14 @@ class ChatListCard extends HookConsumerWidget {
}
class FortuneCard extends HookConsumerWidget {
const FortuneCard({super.key});
final bool unlimited;
const FortuneCard({super.key, this.unlimited = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fortuneAsync = ref.watch(randomFortuneSayingProvider);
return Card(
final child = Card(
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
@@ -550,16 +554,19 @@ class FortuneCard extends HookConsumerWidget {
Expanded(
child: Text(
fortune.content,
maxLines: 2,
maxLines: unlimited ? null : 2,
overflow: TextOverflow.fade,
),
),
Text('—— ${fortune.source}').bold(),
],
).padding(horizontal: 16);
).padding(horizontal: 16, vertical: unlimited ? 12 : 0);
},
),
).height(48);
);
if (unlimited) return child;
return child.height(48);
}
}
@@ -582,6 +589,7 @@ class _UnauthorizedCard extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
const Gap(16),
const SizedBox(width: double.infinity),
Icon(
Symbols.dashboard_rounded,
size: 64,

View File

@@ -13,12 +13,11 @@ import 'package:island/screens/posts/post_detail.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/attachment_uploader.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/compose_form_fields.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/compose_attachments.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/widgets/post/compose_toolbar.dart';
import 'package:island/widgets/post/publishers_modal.dart';
@@ -88,9 +87,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
}, [state]);
final showPreview = useState(false);
final isAttachmentsExpanded = useState(
true,
); // New state for attachments section
// Initialize publisher once when data is available
useEffect(() {
@@ -274,111 +270,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
),
// Attachments preview
ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments,
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Theme(
data: Theme.of(
context,
).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: isAttachmentsExpanded.value,
onExpansionChanged: (expanded) {
isAttachmentsExpanded.value = expanded;
},
collapsedBackgroundColor: Theme.of(
context,
).colorScheme.surfaceContainer,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachments').tr(),
Text(
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
],
),
children: [
ValueListenableBuilder<Map<int, double?>>(
valueListenable: state.attachmentProgress,
builder: (context, progressMap, _) {
return Wrap(
runSpacing: 8,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.length;
idx++
)
SizedBox(
width: 180,
height: 180,
child: AttachmentPreview(
isCompact: true,
item: attachments[idx],
progress: progressMap[idx],
isUploading: progressMap.containsKey(idx),
onRequestUpload: () async {
final config =
await showModalBottomSheet<
AttachmentUploadConfig
>(
context: context,
isScrollControlled: true,
builder: (context) =>
AttachmentUploaderSheet(
ref: ref,
state: state,
index: idx,
),
);
if (config != null) {
await ComposeLogic.uploadAttachment(
ref,
state,
idx,
poolId: config.poolId,
);
}
},
onUpdate: (value) =>
ComposeLogic.updateAttachment(
state,
value,
idx,
),
onDelete: () =>
ComposeLogic.deleteAttachment(
ref,
state,
idx,
),
onInsert: () =>
ComposeLogic.insertAttachment(
ref,
state,
idx,
),
),
),
],
);
},
),
Gap(16),
],
),
);
},
),
ArticleComposeAttachments(state: state),
],
),
),

View File

@@ -1,21 +1,20 @@
import 'dart:io';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
import 'package:island/talker.dart';
class AnalyticsService {
static final AnalyticsService _instance = AnalyticsService._internal();
factory AnalyticsService() => _instance;
AnalyticsService._internal() {
_init();
}
AnalyticsService._internal();
FirebaseAnalytics? _analytics;
bool _enabled = true;
bool get _supportsAnalytics =>
Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
kIsWeb || (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
void _init() {
void initialize() {
if (!_supportsAnalytics) return;
try {
_analytics = FirebaseAnalytics.instance;

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_app_intents/flutter_app_intents.dart';
import 'package:go_router/go_router.dart';
import 'package:island/models/auth.dart';
@@ -21,7 +22,7 @@ class AppIntentsService {
Dio? _dio;
Future<void> initialize() async {
if (!Platform.isIOS) {
if (kIsWeb || !Platform.isIOS) {
talker.warning('[AppIntents] App Intents only supported on iOS');
return;
}

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:just_audio/just_audio.dart';
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
@@ -9,14 +8,13 @@ import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
import 'package:island/pods/audio.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/notification.dart';
import 'package:island/route.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/talker.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
@@ -98,46 +96,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
if (pkt.type == "notifications.new") {
final notification = SnNotification.fromJson(pkt.data!);
if (_appLifecycleState == AppLifecycleState.resumed) {
// App is focused, show in-app notification
talker.info(
'[Notification] Showing in-app notification: ${notification.title}',
);
if (settings.notifyWithHaptic) {
HapticFeedback.heavyImpact();
}
if (settings.soundEffects) {
final player = AudioPlayer();
await player.setVolume(0.75);
await player.setAudioSource(AudioSource.asset('assets/audio/notification.mp3'));
await player.play();
player.dispose();
}
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top:
(!kIsWeb &&
(Platform.isMacOS ||
Platform.isWindows ||
Platform.isLinux))
? 28
// ignore: use_build_context_synchronously
: MediaQuery.of(context).padding.top + 16,
bottom: 16,
),
);
playNotificationSfx(ref);
ref.read(notificationStateProvider.notifier).add(notification);
} else {
// App is in background, show system notification (only on supported platforms)
if (!kIsWeb && !Platform.isIOS) {

View File

@@ -1,27 +1,21 @@
import 'dart:async';
import 'dart:io';
import 'package:just_audio/just_audio.dart';
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/main.dart';
import 'package:island/pods/audio.dart';
import 'package:island/pods/config.dart';
import 'package:island/route.dart';
import 'package:island/pods/notification.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/talker.dart';
import 'package:island/widgets/app_notification.dart';
import 'package:top_snackbar_flutter/top_snack_bar.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:windows_notification/windows_notification.dart' as winty;
import 'package:windows_notification/notification_message.dart';
import 'package:dio/dio.dart';
// Windows notification instance
winty.WindowsNotification? windowsNotification;
@@ -61,53 +55,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
if (pkt.type == "notifications.new") {
final notification = SnNotification.fromJson(pkt.data!);
if (_appLifecycleState == AppLifecycleState.resumed) {
// App is focused, show in-app notification
talker.info(
'[Notification] Showing in-app notification: ${notification.title}',
);
if (settings.notifyWithHaptic) {
HapticFeedback.heavyImpact();
}
if (settings.soundEffects) {
final player = AudioPlayer();
await player.setVolume(0.75);
await player.setAudioSource(AudioSource.asset('assets/audio/notification.mp3'));
await player.play();
player.dispose();
}
showTopSnackBar(
globalOverlay.currentState!,
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: NotificationCard(notification: notification),
),
),
onTap: () {
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
} else {
// External URLs
launchUrlString(uri);
}
}
},
onDismissed: () {},
dismissType: DismissType.onSwipe,
displayDuration: const Duration(seconds: 5),
snackBarPosition: SnackBarPosition.top,
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 28, // Windows specific padding
bottom: 16,
),
);
playNotificationSfx(ref);
ref.read(notificationStateProvider.notifier).add(notification);
} else {
// App is in background, show Windows system notification
talker.info(

View File

@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/main.dart';
import 'package:island/models/account.dart';
import 'package:island/pods/notification.dart';
import 'package:island/talker.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -163,7 +165,6 @@ String _parseRemoteError(DioException err) {
return message ?? err.toString();
}
// Track active overlay dialogs for dismissal
final List<void Function()> _activeOverlayDialogs = [];
Future<T?> showOverlayDialog<T>({
@@ -229,7 +230,6 @@ Future<T?> showOverlayDialog<T>({
return completer.future;
}
// Close the topmost overlay dialog if any exists
bool closeTopmostOverlayDialog() {
if (_activeOverlayDialogs.isNotEmpty) {
final closeFunc = _activeOverlayDialogs.last;
@@ -378,6 +378,34 @@ Future<bool> showConfirmAlert(
return result ?? false;
}
void showNotification({
required String title,
String content = '',
String subtitle = '',
Map<String, dynamic> meta = const {},
Duration? duration,
}) {
final context = globalOverlay.currentState!.context;
final ref = ProviderScope.containerOf(context);
final notification = SnNotification(
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
id: 'local_${DateTime.now().millisecondsSinceEpoch}',
topic: 'local',
title: title,
subtitle: subtitle,
content: content,
meta: meta,
priority: 0,
viewedAt: null,
accountId: 'local',
);
ref
.read(notificationStateProvider.notifier)
.add(notification, duration: duration);
}
Future<void> openExternalLink(Uri url, WidgetRef ref) async {
final whitelistDomains = ['solian.app', 'solsynth.dev'];
if (whitelistDomains.any(

View File

@@ -17,6 +17,7 @@ import 'package:island/services/event_bus.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/cmp/pattle.dart';
import 'package:island/widgets/notification_overlay.dart';
import 'package:island/widgets/task_overlay.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart';
@@ -125,6 +126,8 @@ class WindowScaffold extends HookConsumerWidget {
);
useEffect(() {
if (kIsWeb) return null;
hotKeyManager.register(
popHotKey,
keyDownHandler: (_) {
@@ -259,6 +262,7 @@ class WindowScaffold extends HookConsumerWidget {
),
_WebSocketIndicator(),
const TaskOverlay(),
const NotificationOverlay(),
if (showPalette.value)
CommandPattleWidget(onDismiss: () => showPalette.value = false),
],
@@ -272,6 +276,7 @@ class WindowScaffold extends HookConsumerWidget {
Positioned.fill(child: child),
_WebSocketIndicator(),
const TaskOverlay(),
const NotificationOverlay(),
if (showPalette.value)
CommandPattleWidget(onDismiss: () => showPalette.value = false),
],

View File

@@ -162,6 +162,7 @@ class AppWrapper extends HookConsumerWidget {
(now.day >= 22 && now.day <= 28);
useEffect(() {
Future(() {
final now = DateTime.now();
if (doesShowSnow) {
isShowSnow.value = true;
@@ -192,6 +193,7 @@ class AppWrapper extends HookConsumerWidget {
settingsNotifier.setAskedReview(true);
}
}
});
return null;
}, []);

View File

@@ -284,7 +284,12 @@ class ChatInput extends HookConsumerWidget {
onAttachmentsChanged([
...attachments,
UniversalFile(
data: XFile.fromData(image, mimeType: "image/jpeg"),
displayName: 'image.jpeg',
data: XFile.fromData(
image,
mimeType: "image/jpeg",
name: 'image.jpeg',
),
type: UniversalFileType.image,
),
]);

View File

@@ -93,6 +93,8 @@ class AttachmentPreview extends HookConsumerWidget {
final Function(UniversalFile)? onUpdate;
final Function? onRequestUpload;
final bool isCompact;
final String? thumbnailId;
final Function(String?)? onSetThumbnail;
const AttachmentPreview({
super.key,
@@ -105,6 +107,8 @@ class AttachmentPreview extends HookConsumerWidget {
this.onUpdate,
this.onInsert,
this.isCompact = false,
this.thumbnailId,
this.onSetThumbnail,
});
// GlobalKey for selector
@@ -453,6 +457,39 @@ class AttachmentPreview extends HookConsumerWidget {
),
),
),
if (thumbnailId != null &&
item.isOnCloud &&
(item.data as SnCloudFile).id == thumbnailId)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
borderRadius: BorderRadius.circular(8),
),
),
),
if (thumbnailId != null &&
item.isOnCloud &&
(item.data as SnCloudFile).id == thumbnailId)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Icon(
Symbols.image,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
);
@@ -468,7 +505,7 @@ class AttachmentPreview extends HookConsumerWidget {
child: innerContentWidget,
).center()
else
IntrinsicHeight(child: innerContentWidget),
IntrinsicHeight(child: innerContentWidget).center(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -641,6 +678,24 @@ class AttachmentPreview extends HookConsumerWidget {
await _showSensitiveDialog(context, ref);
},
),
if (item.isOnCloud &&
item.type == UniversalFileType.image &&
onSetThumbnail != null)
MenuAction(
title: thumbnailId == (item.data as SnCloudFile).id
? 'unsetAsThumbnail'.tr()
: 'setAsThumbnail'.tr(),
image: MenuImage.icon(Symbols.image),
callback: () {
final isCurrentlyThumbnail =
thumbnailId == (item.data as SnCloudFile).id;
if (isCurrentlyThumbnail) {
onSetThumbnail?.call(null);
} else {
onSetThumbnail?.call((item.data as SnCloudFile).id);
}
},
),
],
),
child: contentWidget,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/talker.dart';
@@ -57,10 +58,14 @@ class _UniversalAudioState extends ConsumerState<UniversalAudio> {
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
talker.info('[MediaPlayer] Miss cache: $url');
final token = ref.watch(tokenProvider)?.token;
final serverUrl = ref.read(serverUrlProvider);
final token = ref.read(tokenProvider);
final authHeaders = url.startsWith(serverUrl) && token != null
? {'Authorization': 'AtField ${token.token}'}
: null;
DefaultCacheManager().downloadFile(
url,
authHeaders: {'Authorization': 'AtField $token'},
authHeaders: authHeaders,
);
uri = url;
} else {
@@ -68,7 +73,13 @@ class _UniversalAudioState extends ConsumerState<UniversalAudio> {
talker.info('[MediaPlayer] Hit cache: $url');
}
_player!.open(Media(uri), play: widget.autoplay);
final serverUrl = ref.read(serverUrlProvider);
final token = ref.read(tokenProvider);
final Map<String, String>? httpHeaders = uri.startsWith(serverUrl) && token != null
? {'Authorization': 'AtField ${token.token}'}
: null;
_player!.open(Media(uri, httpHeaders: httpHeaders), play: widget.autoplay);
}
@override

View File

@@ -124,24 +124,6 @@ class FileInfoSheet extends StatelessWidget {
},
),
),
if (item.pool != null)
ListTile(
leading: const Icon(Symbols.calendar_today),
title: Text('File Pool').tr(),
subtitle: Text(
item.pool!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: item.pool!.id));
showSnackBar('fileNameCopied'.tr());
},
),
),
ListTile(
leading: const Icon(Symbols.launch),
title: Text('openInBrowser').tr(),
@@ -176,8 +158,7 @@ class FileInfoSheet extends StatelessWidget {
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
title: Text(
entry.key.contains('-')
? entry.key.split('-').last
: entry.key,
@@ -227,8 +208,7 @@ class FileInfoSheet extends StatelessWidget {
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
title: Text(
entry.key,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
@@ -276,8 +256,7 @@ class FileInfoSheet extends StatelessWidget {
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
title:
Text(
title: Text(
entry.key,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,

View File

@@ -1,11 +1,16 @@
import 'dart:math' as math;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
class UniversalImage extends HookWidget {
class UniversalImage extends HookConsumerWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
@@ -28,11 +33,19 @@ class UniversalImage extends HookWidget {
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final loaded = useState(false);
final isCached = useState<bool?>(null);
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
final serverUrl = ref.watch(serverUrlProvider);
final token = ref.watch(tokenProvider);
final Map<String, String>? httpHeaders =
uri.startsWith(serverUrl) && token != null
? {'Authorization': 'AtField ${token.token}'}
: null;
useEffect(() {
DefaultCacheManager().getFileFromCache(uri).then((fileInfo) {
isCached.value = fileInfo != null;
@@ -73,6 +86,7 @@ class UniversalImage extends HookWidget {
else if (isCached.value!)
CachedNetworkImage(
imageUrl: uri,
httpHeaders: httpHeaders,
fit: fit,
width: width,
height: height,
@@ -84,17 +98,18 @@ class UniversalImage extends HookWidget {
width: width,
height: height,
),
errorWidget: (context, url, error) => useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
errorWidget: (context, url, error) => CachedImageErrorWidget(
useFallbackImage: useFallbackImage,
uri: uri,
blurHash: blurHash,
error: error,
debug: true,
),
)
else
CachedNetworkImage(
imageUrl: uri,
httpHeaders: httpHeaders,
fit: fit,
width: width,
height: height,
@@ -123,13 +138,13 @@ class UniversalImage extends HookWidget {
),
);
},
errorWidget: (context, url, error) => useFallbackImage
? Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('image-broke-$uri'),
)
: SizedBox.shrink(),
errorWidget: (context, url, error) => CachedImageErrorWidget(
useFallbackImage: useFallbackImage,
uri: uri,
blurHash: blurHash,
error: error,
debug: true,
),
),
],
),
@@ -137,6 +152,135 @@ class UniversalImage extends HookWidget {
}
}
class CachedImageErrorWidget extends StatelessWidget {
final bool useFallbackImage;
final String uri;
final String? blurHash;
final dynamic error;
final bool debug;
const CachedImageErrorWidget({
super.key,
required this.useFallbackImage,
required this.uri,
this.blurHash,
this.error,
this.debug = false,
});
int? _extractStatusCode(dynamic error) {
if (error == null) return null;
final errorString = error.toString();
// Check for HttpException with status code
final httpExceptionRegex = RegExp(r'Invalid statusCode: (\d+)');
final match = httpExceptionRegex.firstMatch(errorString);
if (match != null) {
return int.tryParse(match.group(1) ?? '');
}
// Check if error has statusCode property (like DioError)
if (error.response?.statusCode != null) {
return error.response.statusCode;
}
return null;
}
@override
Widget build(BuildContext context) {
if (debug && error != null) {
debugPrint('Image load error for $uri: $error');
}
if (!useFallbackImage) {
return SizedBox.shrink();
}
final statusCode = _extractStatusCode(error);
return LayoutBuilder(
builder: (context, constraints) {
final minDimension = constraints.maxWidth < constraints.maxHeight
? constraints.maxWidth
: constraints.maxHeight;
final iconSize = math.max(
minDimension * 0.3,
28,
); // 30% of the smaller dimension
final hasEnoughSpace = minDimension > 40;
return Stack(
fit: StackFit.expand,
children: [
if (blurHash != null)
BlurHash(hash: blurHash!)
else
Image.asset(
'assets/images/media-offline.jpg',
fit: BoxFit.cover,
key: Key('-$uri'),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getErrorIcon(statusCode),
color: Colors.white,
size: iconSize * 0.5,
shadows: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
if (hasEnoughSpace && statusCode != null) ...[
SizedBox(height: iconSize * 0.1),
Container(
padding: EdgeInsets.symmetric(
horizontal: iconSize * 0.15,
vertical: iconSize * 0.05,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(iconSize * 0.1),
),
child: Text(
statusCode.toString(),
style: TextStyle(
color: Colors.white,
fontSize: iconSize * 0.15,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
),
],
);
},
);
}
IconData _getErrorIcon(int? statusCode) {
switch (statusCode) {
case 403:
case 401:
return Icons.lock_rounded;
case 404:
return Icons.broken_image_rounded;
case 500:
case 502:
case 503:
return Icons.error_rounded;
default:
return Icons.broken_image_rounded;
}
}
}
class AnimatedCircularProgressIndicator extends HookWidget {
final double? value;
final Color? color;

View File

@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
@@ -30,7 +32,14 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
_player = Player();
_videoController = VideoController(_player!);
_player!.open(Media(widget.uri), play: widget.autoplay);
final serverUrl = ref.read(serverUrlProvider);
final token = ref.read(tokenProvider);
final Map<String, String>? httpHeaders =
widget.uri.startsWith(serverUrl) && token != null
? {'Authorization': 'AtField ${token.token}'}
: null;
_player!.open(Media(widget.uri, httpHeaders: httpHeaders), play: widget.autoplay);
}
@override

View File

@@ -38,6 +38,8 @@ class ExtendedRefreshIndicator extends HookConsumerWidget {
);
useEffect(() {
if (kIsWeb) return null;
hotKeyManager.register(
refreshHotKey,
keyDownHandler: (_) {

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/notification.dart';
import 'package:island/route.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';
const double kNotificationBorderRadius = 8;
class NotificationItemWidget extends HookConsumerWidget {
final NotificationItem item;
final VoidCallback onDismiss;
final bool isDesktop;
final Animation<double> progress;
const NotificationItemWidget({
super.key,
required this.item,
required this.onDismiss,
required this.isDesktop,
required this.progress,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return GestureDetector(
onTap: () {
if (item.notification.meta['action_uri'] != null) {
var uri = item.notification.meta['action_uri'] as String;
if (uri.startsWith('solian://')) {
uri = uri.replaceFirst('solian://', '');
}
if (uri.startsWith('/')) {
rootNavigatorKey.currentContext?.push(
item.notification.meta['action_uri'],
);
} else {
launchUrlString(uri);
}
}
},
onHorizontalDragEnd: (details) {
if (details.primaryVelocity! > 100) {
onDismiss();
}
},
onVerticalDragEnd: !isDesktop
? (details) {
if (details.primaryVelocity! < -100) {
onDismiss();
}
}
: null,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: double.infinity),
child: Stack(
children: [
Card(
elevation: 4,
margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(kNotificationBorderRadius),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (item.notification.meta['pfp'] != null)
ProfilePictureWidget(
fileId: item.notification.meta['pfp'],
radius: 12,
).padding(right: 12, top: 2)
else
Icon(
Symbols.info,
color: Theme.of(context).colorScheme.primary,
size: 24,
).padding(right: 12),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.notification.title,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
if (item.notification.content.isNotEmpty)
Text(
item.notification.content,
style: Theme.of(context).textTheme.bodyMedium,
),
if (item.notification.subtitle.isNotEmpty)
Text(
item.notification.subtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
),
AnimatedBuilder(
animation: progress,
builder: (context, child) => LinearProgressIndicator(
value: progress.value,
minHeight: 2,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary.withOpacity(0.5),
),
),
),
],
),
),
Positioned(
top: 4,
right: 4,
child: IconButton(
icon: Icon(
Symbols.close,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
onPressed: onDismiss,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
),
),
],
).clipRRect(all: kNotificationBorderRadius),
),
);
}
}

View File

@@ -0,0 +1,177 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/notification.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/notification_item.dart';
import 'package:styled_widget/styled_widget.dart';
class NotificationOverlay extends HookConsumerWidget {
const NotificationOverlay({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifications = ref.watch(notificationStateProvider);
final isDesktop = isWideScreen(context);
final devicePadding = MediaQuery.paddingOf(context);
final topOffset =
devicePadding.top +
((!kIsWeb &&
(Platform.isMacOS || Platform.isLinux || Platform.isWindows))
? 40
: 16);
if (notifications.isEmpty) {
return const SizedBox.shrink();
}
final itemWidth = isDesktop ? 420.0 : MediaQuery.sizeOf(context).width;
if (isDesktop) {
return Positioned(
top: topOffset,
left: 0,
right: 0,
child: Material(
color: Colors.transparent,
child: Column(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: notifications.asMap().entries.map((entry) {
final item = entry.value;
return AnimatedNotificationItem(
key: Key(item.id),
item: item,
isDesktop: true,
margin: EdgeInsets.symmetric(horizontal: 16),
onDismiss: () {
ref.read(notificationStateProvider.notifier).dismiss(item.id);
},
);
}).toList(),
),
).width(itemWidth).alignment(Alignment.topRight),
);
} else {
// Non-desktop: use Stack with overlapping
const double overlap = 20.0;
return Positioned(
top: topOffset,
left: 0,
right: 0,
child: Material(
color: Colors.transparent,
child: SizedBox(
height: MediaQuery.sizeOf(context).height,
child: Stack(
alignment: Alignment.topCenter,
children: notifications.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Positioned(
top: index * overlap,
left: 16,
right: 16,
child: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(color: Colors.black54, blurRadius: 4.0 + index * 2.0),
],
),
child: AnimatedNotificationItem(
key: Key(item.id),
item: item,
isDesktop: false,
onDismiss: () {
ref
.read(notificationStateProvider.notifier)
.dismiss(item.id);
},
),
),
);
}).toList(),
),
),
).width(itemWidth).alignment(Alignment.topCenter),
);
}
}
}
class AnimatedNotificationItem extends HookConsumerWidget {
final NotificationItem item;
final VoidCallback onDismiss;
final bool isDesktop;
final EdgeInsets? margin;
const AnimatedNotificationItem({
super.key,
required this.item,
required this.onDismiss,
required this.isDesktop,
this.margin,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final animationController = useAnimationController(
duration: const Duration(milliseconds: 300),
reverseDuration: const Duration(milliseconds: 250),
);
final progressController = useAnimationController(duration: item.duration);
final curvedAnimation = CurvedAnimation(
parent: animationController,
curve: Curves.easeOutCubic,
);
final slideTween = Tween<Offset>(
begin: isDesktop ? Offset(1.0, 0.0) : Offset(0.0, -1.0),
end: Offset.zero,
);
final progressAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(progressController);
useEffect(() {
animationController.forward();
progressController.forward();
return null;
}, []);
useEffect(() {
if (item.dismissed) {
animationController.reverse().then((_) {
ref.read(notificationStateProvider.notifier).remove(item.id);
});
}
return null;
}, [item.dismissed]);
return SlideTransition(
position: slideTween.animate(curvedAnimation),
child: SizeTransition(
sizeFactor: curvedAnimation,
axis: Axis.vertical,
child: Padding(
padding: margin ?? EdgeInsets.zero,
child: NotificationItemWidget(
item: item,
isDesktop: isDesktop,
onDismiss: onDismiss,
progress: progressAnimation,
),
),
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
@@ -110,20 +111,25 @@ class ArticleComposeAttachments extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ValueListenableBuilder<String?>(
valueListenable: state.thumbnailId,
builder: (context, thumbnailId, _) {
return ValueListenableBuilder<List<UniversalFile>>(
valueListenable: state.attachments,
builder: (context, attachments, _) {
if (attachments.isEmpty) return const SizedBox.shrink();
return Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
data: Theme.of(
context,
).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('attachments'),
Text('attachments').tr(),
Text(
'articleAttachmentHint',
'articleAttachmentHint'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@@ -147,6 +153,9 @@ class ArticleComposeAttachments extends ConsumerWidget {
item: attachments[idx],
progress: progressMap[idx],
isUploading: progressMap.containsKey(idx),
thumbnailId: thumbnailId,
onSetThumbnail: (id) =>
ComposeLogic.setThumbnail(state, id),
onRequestUpload: () async {
final config =
await showModalBottomSheet<
@@ -170,15 +179,22 @@ class ArticleComposeAttachments extends ConsumerWidget {
);
}
},
onUpdate: (value) => ComposeLogic.updateAttachment(
onUpdate: (value) =>
ComposeLogic.updateAttachment(
state,
value,
idx,
),
onDelete: () =>
ComposeLogic.deleteAttachment(ref, state, idx),
onInsert: () =>
ComposeLogic.insertAttachment(ref, state, idx),
onDelete: () => ComposeLogic.deleteAttachment(
ref,
state,
idx,
),
onInsert: () => ComposeLogic.insertAttachment(
ref,
state,
idx,
),
),
),
],
@@ -191,5 +207,7 @@ class ArticleComposeAttachments extends ConsumerWidget {
);
},
);
},
);
}
}

View File

@@ -49,6 +49,8 @@ class ComposeState {
final ValueNotifier<String?> pollId;
// Linked fund id for this compose session (nullable)
final ValueNotifier<String?> fundId;
// Thumbnail id for article type post (nullable)
final ValueNotifier<String?> thumbnailId;
Timer? _autoSaveTimer;
ComposeState({
@@ -69,8 +71,10 @@ class ComposeState {
this.postType = 0,
String? pollId,
String? fundId,
String? thumbnailId,
}) : pollId = ValueNotifier<String?>(pollId),
fundId = ValueNotifier<String?>(fundId);
fundId = ValueNotifier<String?>(fundId),
thumbnailId = ValueNotifier<String?>(thumbnailId);
void startAutoSave(WidgetRef ref) {
_autoSaveTimer?.cancel();
@@ -121,6 +125,9 @@ class ComposeLogic {
} catch (_) {}
}
// Extract thumbnail ID from meta
final thumbnailId = originalPost?.meta?['thumbnail'] as String?;
return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>(
originalPost?.attachments
@@ -156,11 +163,13 @@ class ComposeLogic {
postType: postType,
pollId: pollId,
fundId: fundId,
thumbnailId: thumbnailId,
);
}
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
final tags = draft.tags.map((tag) => tag.slug).toList();
final thumbnailId = draft.meta?['thumbnail'] as String?;
return ComposeState(
attachments: ValueNotifier<List<UniversalFile>>(
@@ -183,6 +192,7 @@ class ComposeLogic {
pollId: null,
// initialize without fund by default
fundId: null,
thumbnailId: thumbnailId,
);
}
@@ -230,7 +240,9 @@ class ComposeLogic {
visibility: state.visibility.value,
content: state.contentController.text,
type: state.postType,
meta: null,
meta: state.postType == 1 && state.thumbnailId.value != null
? {'thumbnail': state.thumbnailId.value}
: null,
viewsUnique: 0,
viewsTotal: 0,
upvotes: 0,
@@ -302,7 +314,9 @@ class ComposeLogic {
visibility: state.visibility.value,
content: state.contentController.text,
type: state.postType,
meta: null,
meta: state.postType == 1 && state.thumbnailId.value != null
? {'thumbnail': state.thumbnailId.value}
: null,
viewsUnique: 0,
viewsTotal: 0,
upvotes: 0,
@@ -612,6 +626,10 @@ class ComposeLogic {
state.embedView.value = null;
}
static void setThumbnail(ComposeState state, String? thumbnailId) {
state.thumbnailId.value = thumbnailId;
}
static Future<void> pickPoll(
WidgetRef ref,
ComposeState state,
@@ -720,6 +738,8 @@ class ComposeLogic {
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
if (state.pollId.value != null) 'poll_id': state.pollId.value,
if (state.fundId.value != null) 'fund_id': state.fundId.value,
if (state.postType == 1 && state.thumbnailId.value != null)
'thumbnail_id': state.thumbnailId.value,
if (state.embedView.value != null)
'embed_view': state.embedView.value!.toJson(),
};
@@ -811,7 +831,12 @@ class ComposeLogic {
state.attachments.value = [
...state.attachments.value,
UniversalFile(
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
displayName: 'image.jpeg',
data: XFile.fromData(
clipboard,
mimeType: "image/jpeg",
name: 'image.jpeg',
),
type: UniversalFileType.image,
),
];
@@ -867,5 +892,6 @@ class ComposeLogic {
state.embedView.dispose();
state.pollId.dispose();
state.fundId.dispose();
state.thumbnailId.dispose();
}
}

View File

@@ -72,8 +72,7 @@ class ComposeToolbar extends HookConsumerWidget {
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder:
(context) => DraftManagerSheet(
builder: (context) => DraftManagerSheet(
onDraftSelected: (draftId) {
final draft = ref.read(composeStorageProvider)[draftId];
if (draft != null) {
@@ -97,13 +96,14 @@ class ComposeToolbar extends HookConsumerWidget {
final colorScheme = Theme.of(context).colorScheme;
if (isCompact) {
return Container(
color: Theme.of(context).colorScheme.surfaceContainerLow,
padding: EdgeInsets.symmetric(horizontal: 8),
return Material(
elevation: 8,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Row(
child:
Row(
children: [
Expanded(
child: SingleChildScrollView(
@@ -140,7 +140,9 @@ class ComposeToolbar extends HookConsumerWidget {
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.pollId.value != null
? colorScheme.primary.withOpacity(0.15)
? colorScheme.primary.withOpacity(
0.15,
)
: null,
),
),
@@ -153,7 +155,9 @@ class ComposeToolbar extends HookConsumerWidget {
builder: (context, _) {
return IconButton(
onPressed: pickFund,
icon: const Icon(Symbols.account_balance_wallet),
icon: const Icon(
Symbols.account_balance_wallet,
),
tooltip: 'fund'.tr(),
color: colorScheme.primary,
visualDensity: const VisualDensity(
@@ -163,7 +167,9 @@ class ComposeToolbar extends HookConsumerWidget {
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.fundId.value != null
? colorScheme.primary.withOpacity(0.15)
? colorScheme.primary.withOpacity(
0.15,
)
: null,
),
),
@@ -186,7 +192,9 @@ class ComposeToolbar extends HookConsumerWidget {
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
state.embedView.value != null
? colorScheme.primary.withOpacity(0.15)
? colorScheme.primary.withOpacity(
0.15,
)
: null,
),
),
@@ -230,10 +238,11 @@ class ComposeToolbar extends HookConsumerWidget {
),
],
).padding(
horizontal: 8,
horizontal: 16,
top: 4,
bottom:
useSafeArea ? MediaQuery.of(context).padding.bottom + 4 : 4,
bottom: useSafeArea
? MediaQuery.of(context).padding.bottom + 4
: 4,
),
),
),
@@ -246,14 +255,18 @@ class ComposeToolbar extends HookConsumerWidget {
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Row(
child:
Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
UploadMenu(items: uploadMenuItems, isCompact: isCompact),
UploadMenu(
items: uploadMenuItems,
isCompact: isCompact,
),
IconButton(
onPressed: linkAttachment,
icon: const Icon(Symbols.attach_file),
@@ -285,7 +298,9 @@ class ComposeToolbar extends HookConsumerWidget {
builder: (context, _) {
return IconButton(
onPressed: pickFund,
icon: const Icon(Symbols.account_balance_wallet),
icon: const Icon(
Symbols.account_balance_wallet,
),
tooltip: 'fund'.tr(),
color: colorScheme.primary,
style: ButtonStyle(

View File

@@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:island/models/account.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
@@ -1030,6 +1031,16 @@ class PostBody extends ConsumerWidget {
);
}
SnCloudFile? getThumbnailAttachment() {
final thumbnailId = item.meta?['thumbnail'] as String?;
if (thumbnailId == null) return null;
try {
return item.attachments.firstWhere((a) => a.id == thumbnailId);
} catch (_) {
return null;
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -1042,7 +1053,6 @@ class PostBody extends ConsumerWidget {
),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: EdgeInsets.only(
top: 4,
left: renderingPadding.horizontal,
@@ -1050,6 +1060,16 @@ class PostBody extends ConsumerWidget {
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (getThumbnailAttachment() != null)
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(8),
),
child: CloudFileWidget(item: getThumbnailAttachment()!),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
@@ -1064,22 +1084,17 @@ class PostBody extends ConsumerWidget {
if (item.title != null)
Text(
item.title!,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.bold,
style: Theme.of(context).textTheme.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
),
if (item.description != null)
if (item.description?.isNotEmpty ?? false)
Text(
item.description!,
style: Theme.of(context).textTheme.bodyMedium,
)
else
MarkdownTextContent(
content: '${_convertContentToMarkdown(item)}...',
attachments: item.attachments,
noMentionChip: item.fediverseUri != null,
),
],
).padding(horizontal: 16, vertical: 12),
],
),
)
else if ((item.content?.isNotEmpty ?? false) ||

View File

@@ -98,7 +98,7 @@ packages:
source: hosted
version: "2.13.0"
audio_session:
dependency: transitive
dependency: "direct main"
description:
name: audio_session
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"

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.5.0+162
version: 3.5.0+163
environment:
sdk: ^3.8.0
@@ -177,6 +177,7 @@ dependencies:
flutter_app_intents: ^0.7.0
video_thumbnail: ^0.5.6
just_audio: ^0.10.5
audio_session: ^0.2.2
dev_dependencies:
flutter_test: