Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
387d19d85c
|
|||
|
f7b991663f
|
|||
|
3a57f4265b
|
|||
|
d1ee2e5160
|
|||
|
bcd6753ed2
|
|||
|
321ea4458b
|
|||
|
8ad31dad58
|
|||
|
269c17d068
|
|||
|
a9abd777e1
|
|||
|
e24b1fc135
|
|||
|
d5feea52fa
|
|||
|
491252bba9
|
|||
|
4f569fbefd
|
|||
|
476da28b5e
|
|||
|
d639df7623
|
|||
|
e1fc5311d2
|
|||
|
d0e4fde6c2
|
|||
|
9437339b0f
|
|||
|
dd7696132c
|
|||
|
95daa3c28d
|
|||
|
ac5193e1f6
|
|||
|
0328a7736a
|
|||
|
03b332f677
|
|||
|
91b2797fb9
|
@@ -1592,5 +1592,7 @@
|
|||||||
"tasksCount": {
|
"tasksCount": {
|
||||||
"one": "{} task",
|
"one": "{} task",
|
||||||
"other": "{} tasks"
|
"other": "{} tasks"
|
||||||
}
|
},
|
||||||
|
"setAsThumbnail": "Set as thumbnail",
|
||||||
|
"unsetAsThumbnail": "Unset as thumbnail"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
7301DB052F08D99C008390F3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7301DB042F08D99C008390F3 /* SwiftUI.framework */; };
|
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, ); }; };
|
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, ); }; };
|
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 */; };
|
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, ); }; };
|
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, ); }; };
|
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; };
|
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>"; };
|
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; };
|
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>"; };
|
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; };
|
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; };
|
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 */,
|
91E124CE95BCB4DCD890160D /* Pods */,
|
||||||
498A09270B73B217F0279168 /* Frameworks */,
|
498A09270B73B217F0279168 /* Frameworks */,
|
||||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */,
|
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */,
|
||||||
|
73595B162F17FF8000AAD53C /* SfxMessage.caf */,
|
||||||
|
73595B172F17FF8000AAD53C /* SfxNotification.caf */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -695,6 +703,8 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
73595B1B2F17FF8000AAD53C /* SfxMessage.caf in Resources */,
|
||||||
|
73595B1C2F17FF8000AAD53C /* SfxNotification.caf in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -703,6 +713,8 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
73595B832F1803D300AAD53C /* SfxNotification.caf in Resources */,
|
||||||
|
73595B842F1803D300AAD53C /* SfxMessage.caf in Resources */,
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
|||||||
@@ -143,7 +143,12 @@ RoomInputManager useRoomInputManager(WidgetRef ref, String roomId) {
|
|||||||
final newAttachments = [
|
final newAttachments = [
|
||||||
...attachments.value,
|
...attachments.value,
|
||||||
UniversalFile(
|
UniversalFile(
|
||||||
data: XFile.fromData(image, mimeType: "image/jpeg"),
|
displayName: 'image.jpeg',
|
||||||
|
data: XFile.fromData(
|
||||||
|
image,
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
name: 'image.jpeg',
|
||||||
|
),
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:image_picker_android/image_picker_android.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/talker.dart';
|
||||||
import 'package:island/firebase_options.dart';
|
import 'package:island/firebase_options.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/audio.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/theme.dart';
|
import 'package:island/pods/theme.dart';
|
||||||
import 'package:island/pods/userinfo.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();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||||
@@ -196,8 +206,12 @@ void main() async {
|
|||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
retry: (retryCount, error) {
|
retry: (retryCount, error) {
|
||||||
if (error is DioException && error.response?.statusCode == 404) {
|
if (retryCount > 3) return null;
|
||||||
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);
|
return const Duration(milliseconds: 300);
|
||||||
},
|
},
|
||||||
@@ -326,6 +340,9 @@ class IslandApp extends HookConsumerWidget {
|
|||||||
final apiClient = ref.read(apiClientProvider);
|
final apiClient = ref.read(apiClientProvider);
|
||||||
subscribePushNotification(apiClient);
|
subscribePushNotification(apiClient);
|
||||||
initializeLocalNotifications();
|
initializeLocalNotifications();
|
||||||
|
ref.read(audioSessionProvider);
|
||||||
|
ref.read(notificationSfxProvider);
|
||||||
|
ref.read(messageSfxProvider);
|
||||||
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
final wsNotifier = ref.read(websocketStateProvider.notifier);
|
||||||
wsNotifier.connect();
|
wsNotifier.connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
@freezed
|
||||||
sealed class SnCloudFile with _$SnCloudFile {
|
sealed class SnCloudFile with _$SnCloudFile {
|
||||||
const factory SnCloudFile({
|
const factory SnCloudFile({
|
||||||
@@ -45,13 +84,11 @@ sealed class SnCloudFile with _$SnCloudFile {
|
|||||||
required String? description,
|
required String? description,
|
||||||
required Map<String, dynamic>? fileMeta,
|
required Map<String, dynamic>? fileMeta,
|
||||||
required Map<String, dynamic>? userMeta,
|
required Map<String, dynamic>? userMeta,
|
||||||
required SnFilePool? pool,
|
|
||||||
@Default([]) List<int> sensitiveMarks,
|
@Default([]) List<int> sensitiveMarks,
|
||||||
required String? mimeType,
|
required String? mimeType,
|
||||||
required String? hash,
|
required String? hash,
|
||||||
required int size,
|
required int size,
|
||||||
required DateTime? uploadedAt,
|
required DateTime? uploadedAt,
|
||||||
required String? uploadedTo,
|
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required DateTime? deletedAt,
|
required DateTime? deletedAt,
|
||||||
|
|||||||
@@ -279,42 +279,42 @@ as String?,
|
|||||||
|
|
||||||
|
|
||||||
/// @nodoc
|
/// @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;
|
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 SnCloudFile
|
/// Create a copy of SnFileReplica
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@pragma('vm:prefer-inline')
|
@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();
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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
|
@override
|
||||||
String toString() {
|
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
|
/// @nodoc
|
||||||
abstract mixin class $SnCloudFileCopyWith<$Res> {
|
abstract mixin class $SnFileReplicaCopyWith<$Res> {
|
||||||
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
|
factory $SnFileReplicaCopyWith(SnFileReplica value, $Res Function(SnFileReplica) _then) = _$SnFileReplicaCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$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
|
/// @nodoc
|
||||||
class _$SnCloudFileCopyWithImpl<$Res>
|
class _$SnFileReplicaCopyWithImpl<$Res>
|
||||||
implements $SnCloudFileCopyWith<$Res> {
|
implements $SnFileReplicaCopyWith<$Res> {
|
||||||
_$SnCloudFileCopyWithImpl(this._self, this._then);
|
_$SnFileReplicaCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
final SnCloudFile _self;
|
final SnFileReplica _self;
|
||||||
final $Res Function(SnCloudFile) _then;
|
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.
|
/// 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(
|
return _then(_self.copyWith(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
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,objectId: null == objectId ? _self.objectId : objectId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
as String,poolId: null == poolId ? _self.poolId : poolId // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
|
as String,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable
|
as SnFilePool?,storageId: null == storageId ? _self.storageId : storageId // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||||
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
as int,isPrimary: null == isPrimary ? _self.isPrimary : isPrimary // ignore: cast_nullable_to_non_nullable
|
||||||
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
|
as bool,createdAt: null == createdAt ? _self.createdAt : createdAt // 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,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // 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,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 DateTime?,
|
||||||
as String?,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of SnCloudFile
|
/// Create a copy of SnFileReplica
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override
|
@override
|
||||||
@pragma('vm:prefer-inline')
|
@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].
|
/// Adds pattern-matching-related methods to [SnCloudFile].
|
||||||
extension SnCloudFilePatterns on SnCloudFile {
|
extension SnCloudFilePatterns on SnCloudFile {
|
||||||
/// A variant of `map` that fallback to returning `orElse`.
|
/// 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) {
|
switch (_that) {
|
||||||
case _SnCloudFile() when $default != null:
|
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();
|
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) {
|
switch (_that) {
|
||||||
case _SnCloudFile():
|
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`
|
/// 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) {
|
switch (_that) {
|
||||||
case _SnCloudFile() when $default != null:
|
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;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -496,7 +1091,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnCloudFile implements SnCloudFile {
|
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);
|
factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
|
||||||
|
|
||||||
@override final String id;
|
@override final String id;
|
||||||
@@ -520,7 +1115,6 @@ class _SnCloudFile implements SnCloudFile {
|
|||||||
return EqualUnmodifiableMapView(value);
|
return EqualUnmodifiableMapView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override final SnFilePool? pool;
|
|
||||||
final List<int> _sensitiveMarks;
|
final List<int> _sensitiveMarks;
|
||||||
@override@JsonKey() List<int> get sensitiveMarks {
|
@override@JsonKey() List<int> get sensitiveMarks {
|
||||||
if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks;
|
if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks;
|
||||||
@@ -532,7 +1126,6 @@ class _SnCloudFile implements SnCloudFile {
|
|||||||
@override final String? hash;
|
@override final String? hash;
|
||||||
@override final int size;
|
@override final int size;
|
||||||
@override final DateTime? uploadedAt;
|
@override final DateTime? uploadedAt;
|
||||||
@override final String? uploadedTo;
|
|
||||||
@override final DateTime createdAt;
|
@override final DateTime createdAt;
|
||||||
@override final DateTime updatedAt;
|
@override final DateTime updatedAt;
|
||||||
@override final DateTime? deletedAt;
|
@override final DateTime? deletedAt;
|
||||||
@@ -551,16 +1144,16 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
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)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@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
|
@override
|
||||||
String toString() {
|
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;
|
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$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
|
/// @nodoc
|
||||||
@@ -588,21 +1181,19 @@ class __$SnCloudFileCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of SnCloudFile
|
/// Create a copy of SnCloudFile
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? 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(
|
return _then(_SnCloudFile(
|
||||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
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,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,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 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>?,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 Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // 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 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?,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 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 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 DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
|
||||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
as DateTime,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,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 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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,78 @@ const _$UniversalFileTypeEnumMap = {
|
|||||||
UniversalFileType.file: 'file',
|
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(
|
_SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
fileMeta: json['file_meta'] as Map<String, dynamic>?,
|
fileMeta: json['file_meta'] as Map<String, dynamic>?,
|
||||||
userMeta: json['user_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:
|
sensitiveMarks:
|
||||||
(json['sensitive_marks'] as List<dynamic>?)
|
(json['sensitive_marks'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => (e as num).toInt())
|
||||||
@@ -49,7 +112,6 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
|
|||||||
uploadedAt: json['uploaded_at'] == null
|
uploadedAt: json['uploaded_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['uploaded_at'] as String),
|
: DateTime.parse(json['uploaded_at'] as String),
|
||||||
uploadedTo: json['uploaded_to'] as String?,
|
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
deletedAt: json['deleted_at'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
@@ -65,13 +127,11 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
|||||||
'description': instance.description,
|
'description': instance.description,
|
||||||
'file_meta': instance.fileMeta,
|
'file_meta': instance.fileMeta,
|
||||||
'user_meta': instance.userMeta,
|
'user_meta': instance.userMeta,
|
||||||
'pool': instance.pool?.toJson(),
|
|
||||||
'sensitive_marks': instance.sensitiveMarks,
|
'sensitive_marks': instance.sensitiveMarks,
|
||||||
'mime_type': instance.mimeType,
|
'mime_type': instance.mimeType,
|
||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'size': instance.size,
|
'size': instance.size,
|
||||||
'uploaded_at': instance.uploadedAt?.toIso8601String(),
|
'uploaded_at': instance.uploadedAt?.toIso8601String(),
|
||||||
'uploaded_to': instance.uploadedTo,
|
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
|||||||
65
lib/pods/audio.dart
Normal file
65
lib/pods/audio.dart
Normal 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);
|
||||||
|
}
|
||||||
@@ -223,7 +223,7 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
// Add new typing status
|
// Add new typing status
|
||||||
_typingStatuses.add(sender);
|
_typingStatuses.add(sender);
|
||||||
}
|
}
|
||||||
state = List.of(_typingStatuses);
|
if (ref.mounted) state = List.of(_typingStatuses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +243,14 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
// Play sound for new messages when app is unfocused
|
// Play sound for new messages when app is unfocused
|
||||||
if (pkt.type == 'messages.new' &&
|
if (pkt.type == 'messages.new' &&
|
||||||
message.senderId != _chatIdentity.id &&
|
message.senderId != _chatIdentity.id &&
|
||||||
ref.read(appLifecycleStateProvider).value != AppLifecycleState.resumed &&
|
ref.read(appLifecycleStateProvider).value !=
|
||||||
|
AppLifecycleState.resumed &&
|
||||||
ref.read(appSettingsProvider).soundEffects) {
|
ref.read(appSettingsProvider).soundEffects) {
|
||||||
final player = AudioPlayer();
|
final player = AudioPlayer();
|
||||||
await player.setVolume(0.75);
|
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();
|
await player.play();
|
||||||
player.dispose();
|
player.dispose();
|
||||||
}
|
}
|
||||||
@@ -288,4 +291,4 @@ class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
|||||||
_typingCooldownTimer = null;
|
_typingCooldownTimer = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final class ChatSubscribeNotifierProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$chatSubscribeNotifierHash() =>
|
String _$chatSubscribeNotifierHash() =>
|
||||||
r'b7624ae45ace2944a88f8b4d14ddce556c236371';
|
r'944cb0c1b1805050470d4b79c60937f622d7b716';
|
||||||
|
|
||||||
final class ChatSubscribeNotifierFamily extends $Family
|
final class ChatSubscribeNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
99
lib/pods/notification.dart
Normal file
99
lib/pods/notification.dart
Normal 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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/pods/notification.g.dart
Normal file
63
lib/pods/notification.g.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/sphere/publishers/${arg.pubName}/feeds/${arg.feedId}',
|
'/insight/publishers/${arg.pubName}/feeds/${arg.feedId}',
|
||||||
);
|
);
|
||||||
return SnWebFeed.fromJson(response.data);
|
return SnWebFeed.fromJson(response.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -47,7 +47,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
|||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
final url = '/sphere/publishers/${feed.publisherId}/feeds';
|
final url = '/insight/publishers/${feed.publisherId}/feeds';
|
||||||
|
|
||||||
final response = feed.id.isEmpty
|
final response = feed.id.isEmpty
|
||||||
? await client.post(url, data: feed.toJson())
|
? await client.post(url, data: feed.toJson())
|
||||||
@@ -67,7 +67,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
|||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
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(
|
state = AsyncValue.data(
|
||||||
SnWebFeed(
|
SnWebFeed(
|
||||||
id: '',
|
id: '',
|
||||||
@@ -93,7 +93,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
|||||||
try {
|
try {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
await client.post(
|
await client.post(
|
||||||
'/sphere/publishers/${arg.pubName}/feeds/$feedId/scrap',
|
'/insight/publishers/${arg.pubName}/feeds/$feedId/scrap',
|
||||||
options: Options(
|
options: Options(
|
||||||
sendTimeout: const Duration(seconds: 60),
|
sendTimeout: const Duration(seconds: 60),
|
||||||
receiveTimeout: const Duration(seconds: 180),
|
receiveTimeout: const Duration(seconds: 180),
|
||||||
|
|||||||
@@ -105,14 +105,12 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
|
|
||||||
return feedAsync.when(
|
return feedAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error:
|
error: (error, _) => ResponseErrorWidget(
|
||||||
(error, _) => ResponseErrorWidget(
|
error: error,
|
||||||
error: error,
|
onRetry: () => ref.invalidate(
|
||||||
onRetry:
|
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
|
||||||
() => ref.invalidate(
|
),
|
||||||
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
data: (feed) {
|
data: (feed) {
|
||||||
// Initialize form fields if they're empty and we have a feed
|
// Initialize form fields if they're empty and we have a feed
|
||||||
if (titleController.text.isEmpty) {
|
if (titleController.text.isEmpty) {
|
||||||
@@ -140,7 +138,7 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
if (context.mounted) isLoading.value = false;
|
||||||
}
|
}
|
||||||
}, [pubName, feedId, ref, context, isLoading]);
|
}, [pubName, feedId, ref, context, isLoading]);
|
||||||
|
|
||||||
@@ -160,8 +158,8 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onTapOutside:
|
onTapOutside: (_) =>
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -184,8 +182,8 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onTapOutside:
|
onTapOutside: (_) =>
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -197,8 +195,8 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTapOutside:
|
onTapOutside: (_) =>
|
||||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -256,7 +254,12 @@ class WebfeedForm extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 20, vertical: 12);
|
).padding(horizontal: 20, vertical: 12);
|
||||||
|
|
||||||
return Column(children: [Expanded(child: formWidget), buttonsRow]);
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(child: formWidget),
|
||||||
|
buttonsRow,
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class _DashboardGridNarrow extends HookConsumerWidget {
|
|||||||
if (userInfo.value != null && userInfo.value?.activatedAt == null)
|
if (userInfo.value != null && userInfo.value?.activatedAt == null)
|
||||||
AccountUnactivatedCard(),
|
AccountUnactivatedCard(),
|
||||||
CheckInWidget(margin: EdgeInsets.zero),
|
CheckInWidget(margin: EdgeInsets.zero),
|
||||||
FortuneCard(),
|
FortuneCard(unlimited: true),
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 400),
|
constraints: const BoxConstraints(maxHeight: 400),
|
||||||
child: PostFeaturedList(),
|
child: PostFeaturedList(),
|
||||||
@@ -304,19 +304,22 @@ class ClockCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
SingleChildScrollView(
|
||||||
spacing: 5,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: Row(
|
||||||
notableDay.when(
|
spacing: 5,
|
||||||
data: (day) => day == null
|
children: [
|
||||||
? Text('unauthorized').tr()
|
notableDay.when(
|
||||||
: _buildNotableDayText(context, day),
|
data: (day) => day == null
|
||||||
error: (err, _) =>
|
? Text('unauthorized').tr()
|
||||||
Text(err.toString()).fontSize(12),
|
: _buildNotableDayText(context, day),
|
||||||
loading: () =>
|
error: (err, _) =>
|
||||||
const Text('loading').tr().fontSize(12),
|
Text(err.toString()).fontSize(12),
|
||||||
),
|
loading: () =>
|
||||||
],
|
const Text('loading').tr().fontSize(12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -528,13 +531,14 @@ class ChatListCard extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FortuneCard extends HookConsumerWidget {
|
class FortuneCard extends HookConsumerWidget {
|
||||||
const FortuneCard({super.key});
|
final bool unlimited;
|
||||||
|
const FortuneCard({super.key, this.unlimited = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final fortuneAsync = ref.watch(randomFortuneSayingProvider);
|
final fortuneAsync = ref.watch(randomFortuneSayingProvider);
|
||||||
|
|
||||||
return Card(
|
final child = Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
@@ -550,16 +554,19 @@ class FortuneCard extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
fortune.content,
|
fortune.content,
|
||||||
maxLines: 2,
|
maxLines: unlimited ? null : 2,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('—— ${fortune.source}').bold(),
|
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,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
|
const SizedBox(width: double.infinity),
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.dashboard_rounded,
|
Symbols.dashboard_rounded,
|
||||||
size: 64,
|
size: 64,
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import 'package:island/screens/posts/post_detail.dart';
|
|||||||
import 'package:island/services/compose_storage_db.dart';
|
import 'package:island/services/compose_storage_db.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/app_scaffold.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/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown.dart';
|
import 'package:island/widgets/content/markdown.dart';
|
||||||
import 'package:island/widgets/post/compose_form_fields.dart';
|
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||||
import 'package:island/widgets/post/compose_shared.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_settings_sheet.dart';
|
||||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||||
import 'package:island/widgets/post/publishers_modal.dart';
|
import 'package:island/widgets/post/publishers_modal.dart';
|
||||||
@@ -88,9 +87,6 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
final showPreview = useState(false);
|
final showPreview = useState(false);
|
||||||
final isAttachmentsExpanded = useState(
|
|
||||||
true,
|
|
||||||
); // New state for attachments section
|
|
||||||
|
|
||||||
// Initialize publisher once when data is available
|
// Initialize publisher once when data is available
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@@ -274,111 +270,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Attachments preview
|
// Attachments preview
|
||||||
ValueListenableBuilder<List<UniversalFile>>(
|
ArticleComposeAttachments(state: state),
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
class AnalyticsService {
|
class AnalyticsService {
|
||||||
static final AnalyticsService _instance = AnalyticsService._internal();
|
static final AnalyticsService _instance = AnalyticsService._internal();
|
||||||
factory AnalyticsService() => _instance;
|
factory AnalyticsService() => _instance;
|
||||||
AnalyticsService._internal() {
|
AnalyticsService._internal();
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
FirebaseAnalytics? _analytics;
|
FirebaseAnalytics? _analytics;
|
||||||
bool _enabled = true;
|
bool _enabled = true;
|
||||||
|
|
||||||
bool get _supportsAnalytics =>
|
bool get _supportsAnalytics =>
|
||||||
Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
kIsWeb || (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||||
|
|
||||||
void _init() {
|
void initialize() {
|
||||||
if (!_supportsAnalytics) return;
|
if (!_supportsAnalytics) return;
|
||||||
try {
|
try {
|
||||||
_analytics = FirebaseAnalytics.instance;
|
_analytics = FirebaseAnalytics.instance;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_app_intents/flutter_app_intents.dart';
|
import 'package:flutter_app_intents/flutter_app_intents.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:island/models/auth.dart';
|
import 'package:island/models/auth.dart';
|
||||||
@@ -21,7 +22,7 @@ class AppIntentsService {
|
|||||||
Dio? _dio;
|
Dio? _dio;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (!Platform.isIOS) {
|
if (kIsWeb || !Platform.isIOS) {
|
||||||
talker.warning('[AppIntents] App Intents only supported on iOS');
|
talker.warning('[AppIntents] App Intents only supported on iOS');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:just_audio/just_audio.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.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_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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/config.dart';
|
||||||
|
import 'package:island/pods/notification.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/talker.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:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
@@ -98,46 +96,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
if (pkt.type == "notifications.new") {
|
if (pkt.type == "notifications.new") {
|
||||||
final notification = SnNotification.fromJson(pkt.data!);
|
final notification = SnNotification.fromJson(pkt.data!);
|
||||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||||
// App is focused, show in-app notification
|
|
||||||
talker.info(
|
talker.info(
|
||||||
'[Notification] Showing in-app notification: ${notification.title}',
|
'[Notification] Showing in-app notification: ${notification.title}',
|
||||||
);
|
);
|
||||||
if (settings.notifyWithHaptic) {
|
if (settings.notifyWithHaptic) {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
}
|
}
|
||||||
if (settings.soundEffects) {
|
playNotificationSfx(ref);
|
||||||
final player = AudioPlayer();
|
ref.read(notificationStateProvider.notifier).add(notification);
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// App is in background, show system notification (only on supported platforms)
|
// App is in background, show system notification (only on supported platforms)
|
||||||
if (!kIsWeb && !Platform.isIOS) {
|
if (!kIsWeb && !Platform.isIOS) {
|
||||||
@@ -228,4 +194,4 @@ Future<void> _putTokenToRemote(
|
|||||||
"/ring/notifications/subscription",
|
"/ring/notifications/subscription",
|
||||||
data: {"provider": provider, "device_token": token},
|
data: {"provider": provider, "device_token": token},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:island/pods/audio.dart';
|
||||||
import 'package:island/main.dart';
|
|
||||||
import 'package:island/pods/config.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/models/account.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/talker.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:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:windows_notification/windows_notification.dart' as winty;
|
import 'package:windows_notification/windows_notification.dart' as winty;
|
||||||
import 'package:windows_notification/notification_message.dart';
|
import 'package:windows_notification/notification_message.dart';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
|
|
||||||
// Windows notification instance
|
// Windows notification instance
|
||||||
winty.WindowsNotification? windowsNotification;
|
winty.WindowsNotification? windowsNotification;
|
||||||
|
|
||||||
@@ -61,53 +55,14 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
if (pkt.type == "notifications.new") {
|
if (pkt.type == "notifications.new") {
|
||||||
final notification = SnNotification.fromJson(pkt.data!);
|
final notification = SnNotification.fromJson(pkt.data!);
|
||||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||||
// App is focused, show in-app notification
|
|
||||||
talker.info(
|
talker.info(
|
||||||
'[Notification] Showing in-app notification: ${notification.title}',
|
'[Notification] Showing in-app notification: ${notification.title}',
|
||||||
);
|
);
|
||||||
if (settings.notifyWithHaptic) {
|
if (settings.notifyWithHaptic) {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
}
|
}
|
||||||
if (settings.soundEffects) {
|
playNotificationSfx(ref);
|
||||||
final player = AudioPlayer();
|
ref.read(notificationStateProvider.notifier).add(notification);
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// App is in background, show Windows system notification
|
// App is in background, show Windows system notification
|
||||||
talker.info(
|
talker.info(
|
||||||
@@ -221,4 +176,4 @@ Future<void> _putTokenToRemote(
|
|||||||
"/ring/notifications/subscription",
|
"/ring/notifications/subscription",
|
||||||
data: {"provider": provider, "device_token": token},
|
data: {"provider": provider, "device_token": token},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/main.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:island/talker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -163,7 +165,6 @@ String _parseRemoteError(DioException err) {
|
|||||||
return message ?? err.toString();
|
return message ?? err.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track active overlay dialogs for dismissal
|
|
||||||
final List<void Function()> _activeOverlayDialogs = [];
|
final List<void Function()> _activeOverlayDialogs = [];
|
||||||
|
|
||||||
Future<T?> showOverlayDialog<T>({
|
Future<T?> showOverlayDialog<T>({
|
||||||
@@ -229,7 +230,6 @@ Future<T?> showOverlayDialog<T>({
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the topmost overlay dialog if any exists
|
|
||||||
bool closeTopmostOverlayDialog() {
|
bool closeTopmostOverlayDialog() {
|
||||||
if (_activeOverlayDialogs.isNotEmpty) {
|
if (_activeOverlayDialogs.isNotEmpty) {
|
||||||
final closeFunc = _activeOverlayDialogs.last;
|
final closeFunc = _activeOverlayDialogs.last;
|
||||||
@@ -378,6 +378,34 @@ Future<bool> showConfirmAlert(
|
|||||||
return result ?? false;
|
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 {
|
Future<void> openExternalLink(Uri url, WidgetRef ref) async {
|
||||||
final whitelistDomains = ['solian.app', 'solsynth.dev'];
|
final whitelistDomains = ['solian.app', 'solsynth.dev'];
|
||||||
if (whitelistDomains.any(
|
if (whitelistDomains.any(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:island/services/event_bus.dart';
|
|||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/cmp/pattle.dart';
|
import 'package:island/widgets/cmp/pattle.dart';
|
||||||
|
import 'package:island/widgets/notification_overlay.dart';
|
||||||
import 'package:island/widgets/task_overlay.dart';
|
import 'package:island/widgets/task_overlay.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -125,6 +126,8 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
if (kIsWeb) return null;
|
||||||
|
|
||||||
hotKeyManager.register(
|
hotKeyManager.register(
|
||||||
popHotKey,
|
popHotKey,
|
||||||
keyDownHandler: (_) {
|
keyDownHandler: (_) {
|
||||||
@@ -259,6 +262,7 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
_WebSocketIndicator(),
|
_WebSocketIndicator(),
|
||||||
const TaskOverlay(),
|
const TaskOverlay(),
|
||||||
|
const NotificationOverlay(),
|
||||||
if (showPalette.value)
|
if (showPalette.value)
|
||||||
CommandPattleWidget(onDismiss: () => showPalette.value = false),
|
CommandPattleWidget(onDismiss: () => showPalette.value = false),
|
||||||
],
|
],
|
||||||
@@ -272,6 +276,7 @@ class WindowScaffold extends HookConsumerWidget {
|
|||||||
Positioned.fill(child: child),
|
Positioned.fill(child: child),
|
||||||
_WebSocketIndicator(),
|
_WebSocketIndicator(),
|
||||||
const TaskOverlay(),
|
const TaskOverlay(),
|
||||||
|
const NotificationOverlay(),
|
||||||
if (showPalette.value)
|
if (showPalette.value)
|
||||||
CommandPattleWidget(onDismiss: () => showPalette.value = false),
|
CommandPattleWidget(onDismiss: () => showPalette.value = false),
|
||||||
],
|
],
|
||||||
@@ -632,4 +637,4 @@ class _WebSocketIndicator extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,36 +162,38 @@ class AppWrapper extends HookConsumerWidget {
|
|||||||
(now.day >= 22 && now.day <= 28);
|
(now.day >= 22 && now.day <= 28);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final now = DateTime.now();
|
Future(() {
|
||||||
if (doesShowSnow) {
|
final now = DateTime.now();
|
||||||
isShowSnow.value = true;
|
if (doesShowSnow) {
|
||||||
Future.delayed(const Duration(seconds: 60), () {
|
isShowSnow.value = true;
|
||||||
if (!context.mounted) return;
|
Future.delayed(const Duration(seconds: 60), () {
|
||||||
isShowSnow.value = false;
|
|
||||||
Future.delayed(const Duration(seconds: 3), () {
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
isSnowGone.value = true;
|
isShowSnow.value = false;
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
isSnowGone.value = true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.firstLaunchAt == null) {
|
|
||||||
settingsNotifier.setFirstLaunchAt(now.toIso8601String());
|
|
||||||
} else if (!settings.askedReview) {
|
|
||||||
final launchAt = DateTime.parse(settings.firstLaunchAt!);
|
|
||||||
final daysSinceFirstLaunch = now.difference(launchAt).inDays;
|
|
||||||
if (daysSinceFirstLaunch >= 3 &&
|
|
||||||
!kIsWeb &&
|
|
||||||
(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
|
||||||
final InAppReview inAppReview = InAppReview.instance;
|
|
||||||
Future(() async {
|
|
||||||
if (await inAppReview.isAvailable()) {
|
|
||||||
inAppReview.requestReview();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
settingsNotifier.setAskedReview(true);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (settings.firstLaunchAt == null) {
|
||||||
|
settingsNotifier.setFirstLaunchAt(now.toIso8601String());
|
||||||
|
} else if (!settings.askedReview) {
|
||||||
|
final launchAt = DateTime.parse(settings.firstLaunchAt!);
|
||||||
|
final daysSinceFirstLaunch = now.difference(launchAt).inDays;
|
||||||
|
if (daysSinceFirstLaunch >= 3 &&
|
||||||
|
!kIsWeb &&
|
||||||
|
(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
||||||
|
final InAppReview inAppReview = InAppReview.instance;
|
||||||
|
Future(() async {
|
||||||
|
if (await inAppReview.isAvailable()) {
|
||||||
|
inAppReview.requestReview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
settingsNotifier.setAskedReview(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -284,7 +284,12 @@ class ChatInput extends HookConsumerWidget {
|
|||||||
onAttachmentsChanged([
|
onAttachmentsChanged([
|
||||||
...attachments,
|
...attachments,
|
||||||
UniversalFile(
|
UniversalFile(
|
||||||
data: XFile.fromData(image, mimeType: "image/jpeg"),
|
displayName: 'image.jpeg',
|
||||||
|
data: XFile.fromData(
|
||||||
|
image,
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
name: 'image.jpeg',
|
||||||
|
),
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
|||||||
final Function(UniversalFile)? onUpdate;
|
final Function(UniversalFile)? onUpdate;
|
||||||
final Function? onRequestUpload;
|
final Function? onRequestUpload;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final String? thumbnailId;
|
||||||
|
final Function(String?)? onSetThumbnail;
|
||||||
|
|
||||||
const AttachmentPreview({
|
const AttachmentPreview({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -105,6 +107,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
|||||||
this.onUpdate,
|
this.onUpdate,
|
||||||
this.onInsert,
|
this.onInsert,
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
|
this.thumbnailId,
|
||||||
|
this.onSetThumbnail,
|
||||||
});
|
});
|
||||||
|
|
||||||
// GlobalKey for selector
|
// 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,
|
child: innerContentWidget,
|
||||||
).center()
|
).center()
|
||||||
else
|
else
|
||||||
IntrinsicHeight(child: innerContentWidget),
|
IntrinsicHeight(child: innerContentWidget).center(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -641,6 +678,24 @@ class AttachmentPreview extends HookConsumerWidget {
|
|||||||
await _showSensitiveDialog(context, ref);
|
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,
|
child: contentWidget,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
@@ -57,10 +58,14 @@ class _UniversalAudioState extends ConsumerState<UniversalAudio> {
|
|||||||
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
||||||
if (inCacheInfo == null) {
|
if (inCacheInfo == null) {
|
||||||
talker.info('[MediaPlayer] Miss cache: $url');
|
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(
|
DefaultCacheManager().downloadFile(
|
||||||
url,
|
url,
|
||||||
authHeaders: {'Authorization': 'AtField $token'},
|
authHeaders: authHeaders,
|
||||||
);
|
);
|
||||||
uri = url;
|
uri = url;
|
||||||
} else {
|
} else {
|
||||||
@@ -68,7 +73,13 @@ class _UniversalAudioState extends ConsumerState<UniversalAudio> {
|
|||||||
talker.info('[MediaPlayer] Hit cache: $url');
|
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
|
@override
|
||||||
@@ -164,4 +175,4 @@ class _UniversalAudioState extends ConsumerState<UniversalAudio> {
|
|||||||
).padding(horizontal: 24, vertical: 16),
|
).padding(horizontal: 24, vertical: 16),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
ListTile(
|
||||||
leading: const Icon(Symbols.launch),
|
leading: const Icon(Symbols.launch),
|
||||||
title: Text('openInBrowser').tr(),
|
title: Text('openInBrowser').tr(),
|
||||||
@@ -176,15 +158,14 @@ class FileInfoSheet extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
title:
|
title: Text(
|
||||||
Text(
|
entry.key.contains('-')
|
||||||
entry.key.contains('-')
|
? entry.key.split('-').last
|
||||||
? entry.key.split('-').last
|
: entry.key,
|
||||||
: entry.key,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
),
|
).bold(),
|
||||||
).bold(),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${entry.value}'.isNotEmpty
|
'${entry.value}'.isNotEmpty
|
||||||
? '${entry.value}'
|
? '${entry.value}'
|
||||||
@@ -227,13 +208,12 @@ class FileInfoSheet extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
title:
|
title: Text(
|
||||||
Text(
|
entry.key,
|
||||||
entry.key,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
),
|
).bold(),
|
||||||
).bold(),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
jsonEncode(entry.value),
|
jsonEncode(entry.value),
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
@@ -276,13 +256,12 @@ class FileInfoSheet extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
title:
|
title: Text(
|
||||||
Text(
|
entry.key,
|
||||||
entry.key,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
fontWeight: FontWeight.w500,
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
),
|
).bold(),
|
||||||
).bold(),
|
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
jsonEncode(entry.value),
|
jsonEncode(entry.value),
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.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 uri;
|
||||||
final String? blurHash;
|
final String? blurHash;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
@@ -28,11 +33,19 @@ class UniversalImage extends HookWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final loaded = useState(false);
|
final loaded = useState(false);
|
||||||
final isCached = useState<bool?>(null);
|
final isCached = useState<bool?>(null);
|
||||||
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
|
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(() {
|
useEffect(() {
|
||||||
DefaultCacheManager().getFileFromCache(uri).then((fileInfo) {
|
DefaultCacheManager().getFileFromCache(uri).then((fileInfo) {
|
||||||
isCached.value = fileInfo != null;
|
isCached.value = fileInfo != null;
|
||||||
@@ -73,6 +86,7 @@ class UniversalImage extends HookWidget {
|
|||||||
else if (isCached.value!)
|
else if (isCached.value!)
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
imageUrl: uri,
|
imageUrl: uri,
|
||||||
|
httpHeaders: httpHeaders,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -84,17 +98,18 @@ class UniversalImage extends HookWidget {
|
|||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) => useFallbackImage
|
errorWidget: (context, url, error) => CachedImageErrorWidget(
|
||||||
? Image.asset(
|
useFallbackImage: useFallbackImage,
|
||||||
'assets/images/media-offline.jpg',
|
uri: uri,
|
||||||
fit: BoxFit.cover,
|
blurHash: blurHash,
|
||||||
key: Key('image-broke-$uri'),
|
error: error,
|
||||||
)
|
debug: true,
|
||||||
: SizedBox.shrink(),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
imageUrl: uri,
|
imageUrl: uri,
|
||||||
|
httpHeaders: httpHeaders,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
@@ -123,13 +138,13 @@ class UniversalImage extends HookWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
errorWidget: (context, url, error) => useFallbackImage
|
errorWidget: (context, url, error) => CachedImageErrorWidget(
|
||||||
? Image.asset(
|
useFallbackImage: useFallbackImage,
|
||||||
'assets/images/media-offline.jpg',
|
uri: uri,
|
||||||
fit: BoxFit.cover,
|
blurHash: blurHash,
|
||||||
key: Key('image-broke-$uri'),
|
error: error,
|
||||||
)
|
debug: true,
|
||||||
: SizedBox.shrink(),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -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 {
|
class AnimatedCircularProgressIndicator extends HookWidget {
|
||||||
final double? value;
|
final double? value;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
@@ -172,4 +316,4 @@ class AnimatedCircularProgressIndicator extends HookWidget {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'dart:io';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
@@ -30,7 +32,14 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
|||||||
_player = Player();
|
_player = Player();
|
||||||
_videoController = VideoController(_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
|
@override
|
||||||
@@ -61,4 +70,4 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
|||||||
: MaterialDesktopVideoControls,
|
: MaterialDesktopVideoControls,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,8 @@ class ExtendedRefreshIndicator extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
if (kIsWeb) return null;
|
||||||
|
|
||||||
hotKeyManager.register(
|
hotKeyManager.register(
|
||||||
refreshHotKey,
|
refreshHotKey,
|
||||||
keyDownHandler: (_) {
|
keyDownHandler: (_) {
|
||||||
|
|||||||
150
lib/widgets/notification_item.dart
Normal file
150
lib/widgets/notification_item.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
lib/widgets/notification_overlay.dart
Normal file
177
lib/widgets/notification_overlay.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
@@ -110,84 +111,101 @@ class ArticleComposeAttachments extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ValueListenableBuilder<List<UniversalFile>>(
|
return ValueListenableBuilder<String?>(
|
||||||
valueListenable: state.attachments,
|
valueListenable: state.thumbnailId,
|
||||||
builder: (context, attachments, _) {
|
builder: (context, thumbnailId, _) {
|
||||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
return ValueListenableBuilder<List<UniversalFile>>(
|
||||||
return Theme(
|
valueListenable: state.attachments,
|
||||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
builder: (context, attachments, _) {
|
||||||
child: ExpansionTile(
|
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||||
initiallyExpanded: true,
|
return Theme(
|
||||||
title: Column(
|
data: Theme.of(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
context,
|
||||||
children: [
|
).copyWith(dividerColor: Colors.transparent),
|
||||||
Text('attachments'),
|
child: ExpansionTile(
|
||||||
Text(
|
initiallyExpanded: true,
|
||||||
'articleAttachmentHint',
|
title: Column(
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
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?>>(
|
||||||
children: [
|
valueListenable: state.attachmentProgress,
|
||||||
ValueListenableBuilder<Map<int, double?>>(
|
builder: (context, progressMap, _) {
|
||||||
valueListenable: state.attachmentProgress,
|
return Wrap(
|
||||||
builder: (context, progressMap, _) {
|
runSpacing: 8,
|
||||||
return Wrap(
|
spacing: 8,
|
||||||
runSpacing: 8,
|
children: [
|
||||||
spacing: 8,
|
for (var idx = 0; idx < attachments.length; idx++)
|
||||||
children: [
|
SizedBox(
|
||||||
for (var idx = 0; idx < attachments.length; idx++)
|
width: 180,
|
||||||
SizedBox(
|
height: 180,
|
||||||
width: 180,
|
child: AttachmentPreview(
|
||||||
height: 180,
|
isCompact: true,
|
||||||
child: AttachmentPreview(
|
item: attachments[idx],
|
||||||
isCompact: true,
|
progress: progressMap[idx],
|
||||||
item: attachments[idx],
|
isUploading: progressMap.containsKey(idx),
|
||||||
progress: progressMap[idx],
|
thumbnailId: thumbnailId,
|
||||||
isUploading: progressMap.containsKey(idx),
|
onSetThumbnail: (id) =>
|
||||||
onRequestUpload: () async {
|
ComposeLogic.setThumbnail(state, id),
|
||||||
final config =
|
onRequestUpload: () async {
|
||||||
await showModalBottomSheet<
|
final config =
|
||||||
AttachmentUploadConfig
|
await showModalBottomSheet<
|
||||||
>(
|
AttachmentUploadConfig
|
||||||
context: context,
|
>(
|
||||||
isScrollControlled: true,
|
context: context,
|
||||||
builder: (context) =>
|
isScrollControlled: true,
|
||||||
AttachmentUploaderSheet(
|
builder: (context) =>
|
||||||
ref: ref,
|
AttachmentUploaderSheet(
|
||||||
state: state,
|
ref: ref,
|
||||||
index: idx,
|
state: state,
|
||||||
),
|
index: idx,
|
||||||
);
|
),
|
||||||
if (config != null) {
|
);
|
||||||
await ComposeLogic.uploadAttachment(
|
if (config != null) {
|
||||||
|
await ComposeLogic.uploadAttachment(
|
||||||
|
ref,
|
||||||
|
state,
|
||||||
|
idx,
|
||||||
|
poolId: config.poolId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: (value) =>
|
||||||
|
ComposeLogic.updateAttachment(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
idx,
|
||||||
|
),
|
||||||
|
onDelete: () => ComposeLogic.deleteAttachment(
|
||||||
ref,
|
ref,
|
||||||
state,
|
state,
|
||||||
idx,
|
idx,
|
||||||
poolId: config.poolId,
|
),
|
||||||
);
|
onInsert: () => ComposeLogic.insertAttachment(
|
||||||
}
|
ref,
|
||||||
},
|
state,
|
||||||
onUpdate: (value) => ComposeLogic.updateAttachment(
|
idx,
|
||||||
state,
|
),
|
||||||
value,
|
),
|
||||||
idx,
|
|
||||||
),
|
),
|
||||||
onDelete: () =>
|
],
|
||||||
ComposeLogic.deleteAttachment(ref, state, idx),
|
);
|
||||||
onInsert: () =>
|
},
|
||||||
ComposeLogic.insertAttachment(ref, state, idx),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
);
|
||||||
],
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class ComposeState {
|
|||||||
final ValueNotifier<String?> pollId;
|
final ValueNotifier<String?> pollId;
|
||||||
// Linked fund id for this compose session (nullable)
|
// Linked fund id for this compose session (nullable)
|
||||||
final ValueNotifier<String?> fundId;
|
final ValueNotifier<String?> fundId;
|
||||||
|
// Thumbnail id for article type post (nullable)
|
||||||
|
final ValueNotifier<String?> thumbnailId;
|
||||||
Timer? _autoSaveTimer;
|
Timer? _autoSaveTimer;
|
||||||
|
|
||||||
ComposeState({
|
ComposeState({
|
||||||
@@ -69,8 +71,10 @@ class ComposeState {
|
|||||||
this.postType = 0,
|
this.postType = 0,
|
||||||
String? pollId,
|
String? pollId,
|
||||||
String? fundId,
|
String? fundId,
|
||||||
|
String? thumbnailId,
|
||||||
}) : pollId = ValueNotifier<String?>(pollId),
|
}) : pollId = ValueNotifier<String?>(pollId),
|
||||||
fundId = ValueNotifier<String?>(fundId);
|
fundId = ValueNotifier<String?>(fundId),
|
||||||
|
thumbnailId = ValueNotifier<String?>(thumbnailId);
|
||||||
|
|
||||||
void startAutoSave(WidgetRef ref) {
|
void startAutoSave(WidgetRef ref) {
|
||||||
_autoSaveTimer?.cancel();
|
_autoSaveTimer?.cancel();
|
||||||
@@ -121,6 +125,9 @@ class ComposeLogic {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract thumbnail ID from meta
|
||||||
|
final thumbnailId = originalPost?.meta?['thumbnail'] as String?;
|
||||||
|
|
||||||
return ComposeState(
|
return ComposeState(
|
||||||
attachments: ValueNotifier<List<UniversalFile>>(
|
attachments: ValueNotifier<List<UniversalFile>>(
|
||||||
originalPost?.attachments
|
originalPost?.attachments
|
||||||
@@ -156,11 +163,13 @@ class ComposeLogic {
|
|||||||
postType: postType,
|
postType: postType,
|
||||||
pollId: pollId,
|
pollId: pollId,
|
||||||
fundId: fundId,
|
fundId: fundId,
|
||||||
|
thumbnailId: thumbnailId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
static ComposeState createStateFromDraft(SnPost draft, {int postType = 0}) {
|
||||||
final tags = draft.tags.map((tag) => tag.slug).toList();
|
final tags = draft.tags.map((tag) => tag.slug).toList();
|
||||||
|
final thumbnailId = draft.meta?['thumbnail'] as String?;
|
||||||
|
|
||||||
return ComposeState(
|
return ComposeState(
|
||||||
attachments: ValueNotifier<List<UniversalFile>>(
|
attachments: ValueNotifier<List<UniversalFile>>(
|
||||||
@@ -183,6 +192,7 @@ class ComposeLogic {
|
|||||||
pollId: null,
|
pollId: null,
|
||||||
// initialize without fund by default
|
// initialize without fund by default
|
||||||
fundId: null,
|
fundId: null,
|
||||||
|
thumbnailId: thumbnailId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +240,9 @@ class ComposeLogic {
|
|||||||
visibility: state.visibility.value,
|
visibility: state.visibility.value,
|
||||||
content: state.contentController.text,
|
content: state.contentController.text,
|
||||||
type: state.postType,
|
type: state.postType,
|
||||||
meta: null,
|
meta: state.postType == 1 && state.thumbnailId.value != null
|
||||||
|
? {'thumbnail': state.thumbnailId.value}
|
||||||
|
: null,
|
||||||
viewsUnique: 0,
|
viewsUnique: 0,
|
||||||
viewsTotal: 0,
|
viewsTotal: 0,
|
||||||
upvotes: 0,
|
upvotes: 0,
|
||||||
@@ -302,7 +314,9 @@ class ComposeLogic {
|
|||||||
visibility: state.visibility.value,
|
visibility: state.visibility.value,
|
||||||
content: state.contentController.text,
|
content: state.contentController.text,
|
||||||
type: state.postType,
|
type: state.postType,
|
||||||
meta: null,
|
meta: state.postType == 1 && state.thumbnailId.value != null
|
||||||
|
? {'thumbnail': state.thumbnailId.value}
|
||||||
|
: null,
|
||||||
viewsUnique: 0,
|
viewsUnique: 0,
|
||||||
viewsTotal: 0,
|
viewsTotal: 0,
|
||||||
upvotes: 0,
|
upvotes: 0,
|
||||||
@@ -612,6 +626,10 @@ class ComposeLogic {
|
|||||||
state.embedView.value = null;
|
state.embedView.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void setThumbnail(ComposeState state, String? thumbnailId) {
|
||||||
|
state.thumbnailId.value = thumbnailId;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> pickPoll(
|
static Future<void> pickPoll(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
ComposeState state,
|
ComposeState state,
|
||||||
@@ -720,6 +738,8 @@ class ComposeLogic {
|
|||||||
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
if (state.realm.value != null) 'realm_id': state.realm.value?.id,
|
||||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||||
if (state.fundId.value != null) 'fund_id': state.fundId.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)
|
if (state.embedView.value != null)
|
||||||
'embed_view': state.embedView.value!.toJson(),
|
'embed_view': state.embedView.value!.toJson(),
|
||||||
};
|
};
|
||||||
@@ -811,7 +831,12 @@ class ComposeLogic {
|
|||||||
state.attachments.value = [
|
state.attachments.value = [
|
||||||
...state.attachments.value,
|
...state.attachments.value,
|
||||||
UniversalFile(
|
UniversalFile(
|
||||||
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
|
displayName: 'image.jpeg',
|
||||||
|
data: XFile.fromData(
|
||||||
|
clipboard,
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
name: 'image.jpeg',
|
||||||
|
),
|
||||||
type: UniversalFileType.image,
|
type: UniversalFileType.image,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -867,5 +892,6 @@ class ComposeLogic {
|
|||||||
state.embedView.dispose();
|
state.embedView.dispose();
|
||||||
state.pollId.dispose();
|
state.pollId.dispose();
|
||||||
state.fundId.dispose();
|
state.fundId.dispose();
|
||||||
|
state.thumbnailId.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,18 +72,17 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
builder:
|
builder: (context) => DraftManagerSheet(
|
||||||
(context) => DraftManagerSheet(
|
onDraftSelected: (draftId) {
|
||||||
onDraftSelected: (draftId) {
|
final draft = ref.read(composeStorageProvider)[draftId];
|
||||||
final draft = ref.read(composeStorageProvider)[draftId];
|
if (draft != null) {
|
||||||
if (draft != null) {
|
state.titleController.text = draft.title ?? '';
|
||||||
state.titleController.text = draft.title ?? '';
|
state.descriptionController.text = draft.description ?? '';
|
||||||
state.descriptionController.text = draft.description ?? '';
|
state.contentController.text = draft.content ?? '';
|
||||||
state.contentController.text = draft.content ?? '';
|
state.visibility.value = draft.visibility;
|
||||||
state.visibility.value = draft.visibility;
|
}
|
||||||
}
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,144 +96,154 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
if (isCompact) {
|
if (isCompact) {
|
||||||
return Container(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
elevation: 8,
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Row(
|
child:
|
||||||
children: [
|
Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: SingleChildScrollView(
|
Expanded(
|
||||||
scrollDirection: Axis.horizontal,
|
child: SingleChildScrollView(
|
||||||
child: Row(
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: Row(
|
||||||
UploadMenu(
|
children: [
|
||||||
items: uploadMenuItems,
|
UploadMenu(
|
||||||
isCompact: isCompact,
|
items: uploadMenuItems,
|
||||||
),
|
isCompact: isCompact,
|
||||||
IconButton(
|
),
|
||||||
onPressed: linkAttachment,
|
IconButton(
|
||||||
icon: const Icon(Symbols.attach_file),
|
onPressed: linkAttachment,
|
||||||
tooltip: 'linkAttachment'.tr(),
|
icon: const Icon(Symbols.attach_file),
|
||||||
color: colorScheme.primary,
|
tooltip: 'linkAttachment'.tr(),
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Poll button with visual state when a poll is linked
|
|
||||||
ListenableBuilder(
|
|
||||||
listenable: state.pollId,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: pickPoll,
|
|
||||||
icon: const Icon(Symbols.how_to_vote),
|
|
||||||
tooltip: 'poll'.tr(),
|
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
vertical: -2,
|
vertical: -2,
|
||||||
),
|
),
|
||||||
style: ButtonStyle(
|
),
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
// Poll button with visual state when a poll is linked
|
||||||
state.pollId.value != null
|
ListenableBuilder(
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
listenable: state.pollId,
|
||||||
: null,
|
builder: (context, _) {
|
||||||
),
|
return IconButton(
|
||||||
),
|
onPressed: pickPoll,
|
||||||
);
|
icon: const Icon(Symbols.how_to_vote),
|
||||||
},
|
tooltip: 'poll'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.pollId.value != null
|
||||||
|
? colorScheme.primary.withOpacity(
|
||||||
|
0.15,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Fund button with visual state when a fund is linked
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: state.fundId,
|
||||||
|
builder: (context, _) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: pickFund,
|
||||||
|
icon: const Icon(
|
||||||
|
Symbols.account_balance_wallet,
|
||||||
|
),
|
||||||
|
tooltip: 'fund'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.fundId.value != null
|
||||||
|
? colorScheme.primary.withOpacity(
|
||||||
|
0.15,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Embed button with visual state when embed is present
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: state.embedView,
|
||||||
|
builder: (context, _) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: showEmbedSheet,
|
||||||
|
icon: const Icon(Symbols.iframe),
|
||||||
|
tooltip: 'embedView'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.embedView.value != null
|
||||||
|
? colorScheme.primary.withOpacity(
|
||||||
|
0.15,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// Fund button with visual state when a fund is linked
|
),
|
||||||
ListenableBuilder(
|
|
||||||
listenable: state.fundId,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: pickFund,
|
|
||||||
icon: const Icon(Symbols.account_balance_wallet),
|
|
||||||
tooltip: 'fund'.tr(),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
|
||||||
state.fundId.value != null
|
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Embed button with visual state when embed is present
|
|
||||||
ListenableBuilder(
|
|
||||||
listenable: state.embedView,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: showEmbedSheet,
|
|
||||||
icon: const Icon(Symbols.iframe),
|
|
||||||
tooltip: 'embedView'.tr(),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
|
||||||
state.embedView.value != null
|
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
if (originalPost == null && state.isEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.draft, size: 20),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
onPressed: showDraftManager,
|
||||||
|
tooltip: 'drafts'.tr(),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 48,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (originalPost == null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.save, size: 20),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
onPressed: saveDraft,
|
||||||
|
onLongPress: showDraftManager,
|
||||||
|
tooltip: 'saveDraft'.tr(),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(
|
||||||
|
horizontal: 16,
|
||||||
|
top: 4,
|
||||||
|
bottom: useSafeArea
|
||||||
|
? MediaQuery.of(context).padding.bottom + 4
|
||||||
|
: 4,
|
||||||
),
|
),
|
||||||
if (originalPost == null && state.isEmpty)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.draft, size: 20),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
onPressed: showDraftManager,
|
|
||||||
tooltip: 'drafts'.tr(),
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 32,
|
|
||||||
minHeight: 48,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (originalPost == null)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.save, size: 20),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
onPressed: saveDraft,
|
|
||||||
onLongPress: showDraftManager,
|
|
||||||
tooltip: 'saveDraft'.tr(),
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 32,
|
|
||||||
minHeight: 48,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(
|
|
||||||
horizontal: 8,
|
|
||||||
top: 4,
|
|
||||||
bottom:
|
|
||||||
useSafeArea ? MediaQuery.of(context).padding.bottom + 4 : 4,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -246,102 +255,108 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: Row(
|
child:
|
||||||
children: [
|
Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: SingleChildScrollView(
|
Expanded(
|
||||||
scrollDirection: Axis.horizontal,
|
child: SingleChildScrollView(
|
||||||
child: Row(
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
child: Row(
|
||||||
UploadMenu(items: uploadMenuItems, isCompact: isCompact),
|
children: [
|
||||||
IconButton(
|
UploadMenu(
|
||||||
onPressed: linkAttachment,
|
items: uploadMenuItems,
|
||||||
icon: const Icon(Symbols.attach_file),
|
isCompact: isCompact,
|
||||||
tooltip: 'linkAttachment'.tr(),
|
),
|
||||||
color: colorScheme.primary,
|
IconButton(
|
||||||
),
|
onPressed: linkAttachment,
|
||||||
// Poll button with visual state when a poll is linked
|
icon: const Icon(Symbols.attach_file),
|
||||||
ListenableBuilder(
|
tooltip: 'linkAttachment'.tr(),
|
||||||
listenable: state.pollId,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: pickPoll,
|
|
||||||
icon: const Icon(Symbols.how_to_vote),
|
|
||||||
tooltip: 'poll'.tr(),
|
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
style: ButtonStyle(
|
),
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
// Poll button with visual state when a poll is linked
|
||||||
state.pollId.value != null
|
ListenableBuilder(
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
listenable: state.pollId,
|
||||||
: null,
|
builder: (context, _) {
|
||||||
),
|
return IconButton(
|
||||||
),
|
onPressed: pickPoll,
|
||||||
);
|
icon: const Icon(Symbols.how_to_vote),
|
||||||
},
|
tooltip: 'poll'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.pollId.value != null
|
||||||
|
? colorScheme.primary.withOpacity(0.15)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Fund button with visual state when a fund is linked
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: state.fundId,
|
||||||
|
builder: (context, _) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: pickFund,
|
||||||
|
icon: const Icon(
|
||||||
|
Symbols.account_balance_wallet,
|
||||||
|
),
|
||||||
|
tooltip: 'fund'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.fundId.value != null
|
||||||
|
? colorScheme.primary.withOpacity(0.15)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Embed button with visual state when embed is present
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: state.embedView,
|
||||||
|
builder: (context, _) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: showEmbedSheet,
|
||||||
|
icon: const Icon(Symbols.iframe),
|
||||||
|
tooltip: 'embedView'.tr(),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
state.embedView.value != null
|
||||||
|
? colorScheme.primary.withOpacity(0.15)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// Fund button with visual state when a fund is linked
|
),
|
||||||
ListenableBuilder(
|
|
||||||
listenable: state.fundId,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: pickFund,
|
|
||||||
icon: const Icon(Symbols.account_balance_wallet),
|
|
||||||
tooltip: 'fund'.tr(),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
|
||||||
state.fundId.value != null
|
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Embed button with visual state when embed is present
|
|
||||||
ListenableBuilder(
|
|
||||||
listenable: state.embedView,
|
|
||||||
builder: (context, _) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: showEmbedSheet,
|
|
||||||
icon: const Icon(Symbols.iframe),
|
|
||||||
tooltip: 'embedView'.tr(),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
style: ButtonStyle(
|
|
||||||
backgroundColor: WidgetStatePropertyAll(
|
|
||||||
state.embedView.value != null
|
|
||||||
? colorScheme.primary.withOpacity(0.15)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
if (originalPost == null && state.isEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.draft),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
onPressed: showDraftManager,
|
||||||
|
tooltip: 'drafts'.tr(),
|
||||||
|
)
|
||||||
|
else if (originalPost == null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
color: colorScheme.primary,
|
||||||
|
onPressed: saveDraft,
|
||||||
|
onLongPress: showDraftManager,
|
||||||
|
tooltip: 'saveDraft'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
horizontal: 16,
|
||||||
|
top: 8,
|
||||||
),
|
),
|
||||||
if (originalPost == null && state.isEmpty)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.draft),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
onPressed: showDraftManager,
|
|
||||||
tooltip: 'drafts'.tr(),
|
|
||||||
)
|
|
||||||
else if (originalPost == null)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.save),
|
|
||||||
color: colorScheme.primary,
|
|
||||||
onPressed: saveDraft,
|
|
||||||
onLongPress: showDraftManager,
|
|
||||||
tooltip: 'saveDraft'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
|
||||||
horizontal: 16,
|
|
||||||
top: 8,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:html2md/html2md.dart' as html2md;
|
import 'package:html2md/html2md.dart' as html2md;
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/models/post.dart';
|
import 'package:island/models/post.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/time.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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -1042,7 +1053,6 @@ class PostBody extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
top: 4,
|
top: 4,
|
||||||
left: renderingPadding.horizontal,
|
left: renderingPadding.horizontal,
|
||||||
@@ -1052,33 +1062,38 @@ class PostBody extends ConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Align(
|
if (getThumbnailAttachment() != null)
|
||||||
alignment: Alignment.centerLeft,
|
ClipRRect(
|
||||||
child: Badge(
|
borderRadius: const BorderRadius.vertical(
|
||||||
label: const Text('postArticle').tr(),
|
top: Radius.circular(8),
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
if (item.title != null)
|
|
||||||
Text(
|
|
||||||
item.title!,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
child: CloudFileWidget(item: getThumbnailAttachment()!),
|
||||||
),
|
),
|
||||||
if (item.description != null)
|
Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
item.description!,
|
children: [
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
Align(
|
||||||
)
|
alignment: Alignment.centerLeft,
|
||||||
else
|
child: Badge(
|
||||||
MarkdownTextContent(
|
label: const Text('postArticle').tr(),
|
||||||
content: '${_convertContentToMarkdown(item)}...',
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
attachments: item.attachments,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
noMentionChip: item.fediverseUri != null,
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(4),
|
||||||
|
if (item.title != null)
|
||||||
|
Text(
|
||||||
|
item.title!,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium!
|
||||||
|
.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
if (item.description?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
item.description!,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, vertical: 12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
audio_session:
|
audio_session:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: audio_session
|
name: audio_session
|
||||||
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
|
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.5.0+162
|
version: 3.5.0+163
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.0
|
sdk: ^3.8.0
|
||||||
@@ -177,6 +177,7 @@ dependencies:
|
|||||||
flutter_app_intents: ^0.7.0
|
flutter_app_intents: ^0.7.0
|
||||||
video_thumbnail: ^0.5.6
|
video_thumbnail: ^0.5.6
|
||||||
just_audio: ^0.10.5
|
just_audio: ^0.10.5
|
||||||
|
audio_session: ^0.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user