Compare commits
31 Commits
3.3.0+146
...
faf3a677d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
faf3a677d4
|
|||
|
0f644a0234
|
|||
|
18d16fdd57
|
|||
|
18e890d63c
|
|||
|
9c5e50c16a
|
|||
|
96a2c8182e
|
|||
|
56b27c3e82
|
|||
|
ad4bf94195
|
|||
|
b77a832d8a
|
|||
|
5e61805db7
|
|||
|
35b96b0bd2
|
|||
|
c8ad791ff3
|
|||
|
1e908502dc
|
|||
|
715ce1a368
|
|||
|
548c9963ee
|
|||
|
db5199438a
|
|||
|
4409a6fb1e
|
|||
|
26a24b0e41
|
|||
|
9b948d259b
|
|||
|
1f713b5b2b
|
|||
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
@@ -1336,5 +1336,8 @@
|
||||
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||
"amountOfSplits": "Amount of Splits",
|
||||
"enterNumberOfSplits": "Enter Splits Amount",
|
||||
"orCreateWith": "Or\ncreate with"
|
||||
"orCreateWith": "Or\ncreate with",
|
||||
"unindexedFiles": "Unindexed files",
|
||||
"folder": "Folder",
|
||||
"clearCompleted": "Clear Completed"
|
||||
}
|
||||
|
||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
@@ -57,7 +57,7 @@ PODS:
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- firebase_crashlytics (5.0.5):
|
||||
- Firebase/Crashlytics (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@@ -148,7 +148,7 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- KeychainAccess
|
||||
- flutter_webrtc (1.2.0):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
@@ -216,7 +216,8 @@ PODS:
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- Kingfisher (8.6.1)
|
||||
- KeychainAccess (4.2.2)
|
||||
- Kingfisher (8.6.2)
|
||||
- KingfisherWebP (1.7.2):
|
||||
- Kingfisher (~> 8.0)
|
||||
- libwebp (>= 1.1.0)
|
||||
@@ -269,7 +270,6 @@ PODS:
|
||||
- Flutter
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
@@ -315,8 +315,6 @@ PODS:
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
@@ -368,7 +366,6 @@ DEPENDENCIES:
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -390,6 +387,7 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- KeychainAccess
|
||||
- Kingfisher
|
||||
- KingfisherWebP
|
||||
- libwebp
|
||||
@@ -397,7 +395,6 @@ SPEC REPOS:
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
@@ -490,8 +487,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
@@ -507,7 +502,7 @@ SPEC CHECKSUMS:
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 67fbdd9f3c04e55048024f3da21cfc36f05e56cf
|
||||
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
|
||||
firebase_crashlytics: 83c7467d7534975a4d779af43bd226d0a4616464
|
||||
firebase_crashlytics: c039028126cb45e32f4c217aa392408b0963d081
|
||||
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
@@ -527,7 +522,7 @@ SPEC CHECKSUMS:
|
||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||
@@ -536,7 +531,8 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
Kingfisher: 7ac7a7288653787a54206b11a3c74f49ab650f1f
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
livekit_client: 86c8af579274e4b7a215185a8080db2d4e176f40
|
||||
@@ -555,7 +551,6 @@ SPEC CHECKSUMS:
|
||||
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
@@ -567,7 +562,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@ import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/database/draft.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
|
||||
part 'drift_db.g.dart';
|
||||
|
||||
// Define the database
|
||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
||||
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 7;
|
||||
int get schemaVersion => 8;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -55,6 +57,11 @@ class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (from < 8) {
|
||||
// Add new tables for separate sender and room data
|
||||
await m.createTable(chatRooms);
|
||||
await m.createTable(chatMembers);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -153,6 +160,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
String roomId,
|
||||
String query, {
|
||||
bool? withAttachments,
|
||||
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||
}) async {
|
||||
var selectStatement = select(chatMessages)
|
||||
..where((m) => m.roomId.equals(roomId));
|
||||
@@ -178,7 +186,11 @@ class AppDatabase extends _$AppDatabase {
|
||||
await (selectStatement
|
||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||
.get();
|
||||
return messages.map((msg) => companionToMessage(msg)).toList();
|
||||
final messageFutures =
|
||||
messages
|
||||
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
|
||||
.toList();
|
||||
return await Future.wait(messageFutures);
|
||||
}
|
||||
|
||||
// Convert between Drift and model objects
|
||||
@@ -206,12 +218,88 @@ class AppDatabase extends _$AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
LocalChatMessage companionToMessage(ChatMessage dbMessage) {
|
||||
Future<LocalChatMessage> companionToMessage(
|
||||
ChatMessage dbMessage, {
|
||||
Future<SnAccount?> Function(String accountId)? fetchAccount,
|
||||
}) async {
|
||||
final data = jsonDecode(dbMessage.data);
|
||||
SnChatMember? sender;
|
||||
try {
|
||||
final senderRow =
|
||||
await (select(chatMembers)
|
||||
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
||||
SnAccount senderAccount;
|
||||
senderAccount = SnAccount.fromJson(senderRow.account);
|
||||
|
||||
sender = SnChatMember(
|
||||
id: senderRow.id,
|
||||
chatRoomId: senderRow.chatRoomId,
|
||||
accountId: senderRow.accountId,
|
||||
account: senderAccount,
|
||||
nick: senderRow.nick,
|
||||
role: senderRow.role,
|
||||
notify: senderRow.notify,
|
||||
joinedAt: senderRow.joinedAt,
|
||||
breakUntil: senderRow.breakUntil,
|
||||
timeoutUntil: senderRow.timeoutUntil,
|
||||
isBot: senderRow.isBot,
|
||||
status: null,
|
||||
lastTyped: senderRow.lastTyped,
|
||||
createdAt: senderRow.createdAt,
|
||||
updatedAt: senderRow.updatedAt,
|
||||
deletedAt: senderRow.deletedAt,
|
||||
chatRoom: null,
|
||||
);
|
||||
} catch (err) {
|
||||
// Fallback to dummy sender with senderId as display name
|
||||
sender = SnChatMember(
|
||||
id: 'unknown',
|
||||
chatRoomId: dbMessage.roomId,
|
||||
accountId: dbMessage.senderId,
|
||||
account: SnAccount(
|
||||
id: 'unknown',
|
||||
name: 'unknown',
|
||||
nick: dbMessage.senderId, // Show the ID instead of Unknown
|
||||
profile: SnAccountProfile(
|
||||
picture: null,
|
||||
id: 'unknown',
|
||||
experience: 0,
|
||||
level: 1,
|
||||
levelingProgress: 0.0,
|
||||
background: null,
|
||||
verification: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
),
|
||||
language: '',
|
||||
isSuperuser: false,
|
||||
automatedId: null,
|
||||
perkSubscription: null,
|
||||
deletedAt: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
nick: dbMessage.senderId, // Show the senderId as fallback
|
||||
role: 0,
|
||||
notify: 0,
|
||||
joinedAt: null,
|
||||
breakUntil: null,
|
||||
timeoutUntil: null,
|
||||
isBot: false,
|
||||
status: null,
|
||||
lastTyped: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
chatRoom: null,
|
||||
);
|
||||
}
|
||||
return LocalChatMessage(
|
||||
id: dbMessage.id,
|
||||
roomId: dbMessage.roomId,
|
||||
senderId: dbMessage.senderId,
|
||||
sender: sender,
|
||||
data: data,
|
||||
createdAt: dbMessage.createdAt,
|
||||
status: dbMessage.status,
|
||||
@@ -231,6 +319,65 @@ class AppDatabase extends _$AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
ChatRoomsCompanion companionFromRoom(SnChatRoom room) {
|
||||
return ChatRoomsCompanion(
|
||||
id: Value(room.id),
|
||||
name: Value(room.name),
|
||||
description: Value(room.description),
|
||||
type: Value(room.type),
|
||||
isPublic: Value(room.isPublic),
|
||||
isCommunity: Value(room.isCommunity),
|
||||
picture: Value(room.picture?.toJson()),
|
||||
background: Value(room.background?.toJson()),
|
||||
realmId: Value(room.realmId),
|
||||
createdAt: Value(room.createdAt),
|
||||
updatedAt: Value(room.updatedAt),
|
||||
deletedAt: Value(room.deletedAt),
|
||||
);
|
||||
}
|
||||
|
||||
ChatMembersCompanion companionFromMember(SnChatMember member) {
|
||||
return ChatMembersCompanion(
|
||||
id: Value(member.id),
|
||||
chatRoomId: Value(member.chatRoomId),
|
||||
accountId: Value(member.accountId),
|
||||
account: Value(member.account.toJson()),
|
||||
nick: Value(member.nick),
|
||||
role: Value(member.role),
|
||||
notify: Value(member.notify),
|
||||
joinedAt: Value(member.joinedAt),
|
||||
breakUntil: Value(member.breakUntil),
|
||||
timeoutUntil: Value(member.timeoutUntil),
|
||||
isBot: Value(member.isBot),
|
||||
status: Value(
|
||||
member.status == null ? null : jsonEncode(member.status!.toJson()),
|
||||
),
|
||||
lastTyped: Value(member.lastTyped),
|
||||
createdAt: Value(member.createdAt),
|
||||
updatedAt: Value(member.updatedAt),
|
||||
deletedAt: Value(member.deletedAt),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> saveChatRooms(List<SnChatRoom> rooms) async {
|
||||
await batch((batch) {
|
||||
for (final room in rooms) {
|
||||
batch.insert(
|
||||
chatRooms,
|
||||
companionFromRoom(room),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
for (final member in room.members ?? []) {
|
||||
batch.insert(
|
||||
chatMembers,
|
||||
companionFromMember(member),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Methods for post drafts
|
||||
Future<List<SnPost>> getAllPostDrafts() async {
|
||||
final drafts = await select(postDrafts).get();
|
||||
@@ -276,4 +423,10 @@ class AppDatabase extends _$AppDatabase {
|
||||
return await (select(postDrafts)
|
||||
..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> saveMember(SnChatMember member) async {
|
||||
await into(
|
||||
chatMembers,
|
||||
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
616
lib/database/drift_db.steps.dart
Normal file
616
lib/database/drift_db.steps.dart
Normal file
@@ -0,0 +1,616 @@
|
||||
// dart format width=80
|
||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class Schema7 extends i0.VersionedSchema {
|
||||
Schema7({required super.database}) : super(version: 7);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
chatRooms,
|
||||
chatMembers,
|
||||
chatMessages,
|
||||
postDrafts,
|
||||
];
|
||||
late final Shape0 chatRooms = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'chat_rooms',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape1 chatMembers = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'chat_members',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_12,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_22,
|
||||
_column_23,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 chatMessages = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'chat_messages',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_24,
|
||||
_column_25,
|
||||
_column_26,
|
||||
_column_27,
|
||||
_column_28,
|
||||
_column_9,
|
||||
_column_29,
|
||||
_column_30,
|
||||
_column_31,
|
||||
_column_11,
|
||||
_column_32,
|
||||
_column_33,
|
||||
_column_34,
|
||||
_column_35,
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 postDrafts = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'post_drafts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_40,
|
||||
_column_2,
|
||||
_column_26,
|
||||
_column_41,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
}
|
||||
|
||||
class Shape0 extends i0.VersionedTable {
|
||||
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get description =>
|
||||
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<bool> get isPublic =>
|
||||
columnsByName['is_public']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get isCommunity =>
|
||||
columnsByName['is_community']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get picture =>
|
||||
columnsByName['picture']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get background =>
|
||||
columnsByName['background']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get realmId =>
|
||||
columnsByName['realm_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'name',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'description',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'type',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<bool> _column_4(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_public',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_public" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_community',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_community" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'picture',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'background',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'realm_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'deleted_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
|
||||
class Shape1 extends i0.VersionedTable {
|
||||
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get chatRoomId =>
|
||||
columnsByName['chat_room_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get account =>
|
||||
columnsByName['account']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get nick =>
|
||||
columnsByName['nick']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get role =>
|
||||
columnsByName['role']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get notify =>
|
||||
columnsByName['notify']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get joinedAt =>
|
||||
columnsByName['joined_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get breakUntil =>
|
||||
columnsByName['break_until']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get timeoutUntil =>
|
||||
columnsByName['timeout_until']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<bool> get isBot =>
|
||||
columnsByName['is_bot']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get status =>
|
||||
columnsByName['status']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get lastTyped =>
|
||||
columnsByName['last_typed']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'chat_room_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES chat_rooms (id)',
|
||||
),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'account_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'account',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'nick',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_16(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'role',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'notify',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'joined_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_19(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'break_until',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_20(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'timeout_until',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<bool> _column_21(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_bot',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_bot" IN (0, 1))',
|
||||
),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'status',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_23(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'last_typed',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
|
||||
class Shape2 extends i0.VersionedTable {
|
||||
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get roomId =>
|
||||
columnsByName['room_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get senderId =>
|
||||
columnsByName['sender_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get nonce =>
|
||||
columnsByName['nonce']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get data =>
|
||||
columnsByName['data']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get status =>
|
||||
columnsByName['status']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<bool> get isDeleted =>
|
||||
columnsByName['is_deleted']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get deletedAt =>
|
||||
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get meta =>
|
||||
columnsByName['meta']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get membersMentioned =>
|
||||
columnsByName['members_mentioned']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get editedAt =>
|
||||
columnsByName['edited_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get attachments =>
|
||||
columnsByName['attachments']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get reactions =>
|
||||
columnsByName['reactions']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get repliedMessageId =>
|
||||
columnsByName['replied_message_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get forwardedMessageId =>
|
||||
columnsByName['forwarded_message_id']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'room_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES chat_rooms (id)',
|
||||
),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'sender_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES chat_members (id)',
|
||||
),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'content',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'nonce',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'status',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
);
|
||||
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
|
||||
i1.GeneratedColumn<bool>(
|
||||
'is_deleted',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.bool,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_deleted" IN (0, 1))',
|
||||
),
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_31(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'type',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const CustomExpression('\'text\''),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'meta',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const CustomExpression('\'{}\''),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'members_mentioned',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const CustomExpression('\'[]\''),
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_35(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'edited_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'attachments',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const CustomExpression('\'[]\''),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'reactions',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: const CustomExpression('\'[]\''),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'replied_message_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'forwarded_message_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
|
||||
class Shape3 extends i0.VersionedTable {
|
||||
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get title =>
|
||||
columnsByName['title']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get description =>
|
||||
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get visibility =>
|
||||
columnsByName['visibility']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get lastModified =>
|
||||
columnsByName['last_modified']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<String> get postData =>
|
||||
columnsByName['post_data']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'title',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_41(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'visibility',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'type',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
i1.GeneratedColumn<DateTime> _column_43(String aliasedName) =>
|
||||
i1.GeneratedColumn<DateTime>(
|
||||
'last_modified',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.dateTime,
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_44(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'post_data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
case 6:
|
||||
final schema = Schema7(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from6To7(migrator, schema);
|
||||
return 7;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(from6To7: from6To7),
|
||||
);
|
||||
@@ -36,10 +36,52 @@ class ListMapConverter
|
||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ChatRooms extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text().nullable()();
|
||||
TextColumn get description => text().nullable()();
|
||||
IntColumn get type => integer()();
|
||||
BoolColumn get isPublic =>
|
||||
boolean().nullable().withDefault(const Constant(false))();
|
||||
BoolColumn get isCommunity =>
|
||||
boolean().nullable().withDefault(const Constant(false))();
|
||||
TextColumn get picture => text().map(const MapConverter()).nullable()();
|
||||
TextColumn get background => text().map(const MapConverter()).nullable()();
|
||||
TextColumn get realmId => text().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ChatMembers extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get chatRoomId => text().references(ChatRooms, #id)();
|
||||
TextColumn get accountId => text()();
|
||||
TextColumn get account => text().map(const MapConverter())();
|
||||
TextColumn get nick => text().nullable()();
|
||||
IntColumn get role => integer()();
|
||||
IntColumn get notify => integer()();
|
||||
DateTimeColumn get joinedAt => dateTime().nullable()();
|
||||
DateTimeColumn get breakUntil => dateTime().nullable()();
|
||||
DateTimeColumn get timeoutUntil => dateTime().nullable()();
|
||||
BoolColumn get isBot => boolean()();
|
||||
TextColumn get status => text().nullable()();
|
||||
DateTimeColumn get lastTyped => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ChatMessages extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get roomId => text()();
|
||||
TextColumn get senderId => text()();
|
||||
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||
TextColumn get content => text().nullable()();
|
||||
TextColumn get nonce => text().nullable()();
|
||||
TextColumn get data => text()();
|
||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
||||
final String id;
|
||||
final String roomId;
|
||||
final String senderId;
|
||||
final SnChatMember? sender;
|
||||
final Map<String, dynamic> data;
|
||||
final DateTime createdAt;
|
||||
MessageStatus status;
|
||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
||||
required this.id,
|
||||
required this.roomId,
|
||||
required this.senderId,
|
||||
required this.sender,
|
||||
required this.data,
|
||||
required this.createdAt,
|
||||
required this.nonce,
|
||||
@@ -114,7 +158,12 @@ class LocalChatMessage {
|
||||
});
|
||||
|
||||
SnChatMessage toRemoteMessage() {
|
||||
return SnChatMessage.fromJson(data);
|
||||
if (sender == null) {
|
||||
throw Exception('Cannot create remote message without sender');
|
||||
}
|
||||
final msgData = Map<String, dynamic>.from(data);
|
||||
msgData['sender'] = sender!.toJson();
|
||||
return SnChatMessage.fromJson(msgData);
|
||||
}
|
||||
|
||||
static LocalChatMessage fromRemoteMessage(
|
||||
@@ -122,11 +171,26 @@ class LocalChatMessage {
|
||||
MessageStatus status, {
|
||||
String? nonce,
|
||||
}) {
|
||||
final jsonData = message.toJson();
|
||||
jsonData.remove('sender');
|
||||
// Ensure proper defaults for collections to prevent type cast errors
|
||||
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
|
||||
if (jsonData['members_mentioned'] == null) {
|
||||
jsonData['members_mentioned'] = <String>[];
|
||||
}
|
||||
if (jsonData['attachments'] == null) {
|
||||
jsonData['attachments'] = <Map<String, dynamic>>[];
|
||||
}
|
||||
if (jsonData['reactions'] == null) {
|
||||
jsonData['reactions'] = <Map<String, dynamic>>[];
|
||||
}
|
||||
final msgData = Map<String, dynamic>.from(jsonData);
|
||||
return LocalChatMessage(
|
||||
id: message.id,
|
||||
roomId: message.chatRoomId,
|
||||
senderId: message.senderId,
|
||||
data: message.toJson(),
|
||||
sender: message.sender,
|
||||
data: msgData,
|
||||
createdAt: message.createdAt,
|
||||
status: status,
|
||||
nonce: nonce ?? message.nonce,
|
||||
|
||||
23
lib/models/reference.dart
Normal file
23
lib/models/reference.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
part 'reference.freezed.dart';
|
||||
part 'reference.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class Reference with _$Reference {
|
||||
const factory Reference({
|
||||
required String id,
|
||||
@JsonKey(name: 'file_id') required String fileId,
|
||||
SnCloudFile? file,
|
||||
required String usage,
|
||||
@JsonKey(name: 'resource_id') required String resourceId,
|
||||
@JsonKey(name: 'expired_at') DateTime? expiredAt,
|
||||
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||
@JsonKey(name: 'deleted_at') DateTime? deletedAt,
|
||||
}) = _Reference;
|
||||
|
||||
factory Reference.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReferenceFromJson(json);
|
||||
}
|
||||
319
lib/models/reference.freezed.dart
Normal file
319
lib/models/reference.freezed.dart
Normal file
@@ -0,0 +1,319 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'reference.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Reference {
|
||||
|
||||
String get id;@JsonKey(name: 'file_id') String get fileId; SnCloudFile? get file; String get usage;@JsonKey(name: 'resource_id') String get resourceId;@JsonKey(name: 'expired_at') DateTime? get expiredAt;@JsonKey(name: 'created_at') DateTime get createdAt;@JsonKey(name: 'updated_at') DateTime get updatedAt;@JsonKey(name: 'deleted_at') DateTime? get deletedAt;
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ReferenceCopyWith<Reference> get copyWith => _$ReferenceCopyWithImpl<Reference>(this as Reference, _$identity);
|
||||
|
||||
/// Serializes this Reference to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(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,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ReferenceCopyWith<$Res> {
|
||||
factory $ReferenceCopyWith(Reference value, $Res Function(Reference) _then) = _$ReferenceCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res>? get file;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ReferenceCopyWithImpl<$Res>
|
||||
implements $ReferenceCopyWith<$Res> {
|
||||
_$ReferenceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final Reference _self;
|
||||
final $Res Function(Reference) _then;
|
||||
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // 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?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res>? get file {
|
||||
if (_self.file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||
return _then(_self.copyWith(file: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [Reference].
|
||||
extension ReferencePatterns on Reference {
|
||||
/// 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( _Reference value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference() 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( _Reference value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference():
|
||||
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( _Reference value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference() 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, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference() when $default != null:
|
||||
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_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, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference():
|
||||
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_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, @JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage, @JsonKey(name: 'resource_id') String resourceId, @JsonKey(name: 'expired_at') DateTime? expiredAt, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, @JsonKey(name: 'deleted_at') DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Reference() when $default != null:
|
||||
return $default(_that.id,_that.fileId,_that.file,_that.usage,_that.resourceId,_that.expiredAt,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _Reference implements Reference {
|
||||
const _Reference({required this.id, @JsonKey(name: 'file_id') required this.fileId, this.file, required this.usage, @JsonKey(name: 'resource_id') required this.resourceId, @JsonKey(name: 'expired_at') this.expiredAt, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, @JsonKey(name: 'deleted_at') this.deletedAt});
|
||||
factory _Reference.fromJson(Map<String, dynamic> json) => _$ReferenceFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override@JsonKey(name: 'file_id') final String fileId;
|
||||
@override final SnCloudFile? file;
|
||||
@override final String usage;
|
||||
@override@JsonKey(name: 'resource_id') final String resourceId;
|
||||
@override@JsonKey(name: 'expired_at') final DateTime? expiredAt;
|
||||
@override@JsonKey(name: 'created_at') final DateTime createdAt;
|
||||
@override@JsonKey(name: 'updated_at') final DateTime updatedAt;
|
||||
@override@JsonKey(name: 'deleted_at') final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ReferenceCopyWith<_Reference> get copyWith => __$ReferenceCopyWithImpl<_Reference>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ReferenceToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Reference&&(identical(other.id, id) || other.id == id)&&(identical(other.fileId, fileId) || other.fileId == fileId)&&(identical(other.file, file) || other.file == file)&&(identical(other.usage, usage) || other.usage == usage)&&(identical(other.resourceId, resourceId) || other.resourceId == resourceId)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(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,fileId,file,usage,resourceId,expiredAt,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Reference(id: $id, fileId: $fileId, file: $file, usage: $usage, resourceId: $resourceId, expiredAt: $expiredAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ReferenceCopyWith<$Res> implements $ReferenceCopyWith<$Res> {
|
||||
factory _$ReferenceCopyWith(_Reference value, $Res Function(_Reference) _then) = __$ReferenceCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id,@JsonKey(name: 'file_id') String fileId, SnCloudFile? file, String usage,@JsonKey(name: 'resource_id') String resourceId,@JsonKey(name: 'expired_at') DateTime? expiredAt,@JsonKey(name: 'created_at') DateTime createdAt,@JsonKey(name: 'updated_at') DateTime updatedAt,@JsonKey(name: 'deleted_at') DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnCloudFileCopyWith<$Res>? get file;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ReferenceCopyWithImpl<$Res>
|
||||
implements _$ReferenceCopyWith<$Res> {
|
||||
__$ReferenceCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _Reference _self;
|
||||
final $Res Function(_Reference) _then;
|
||||
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? fileId = null,Object? file = freezed,Object? usage = null,Object? resourceId = null,Object? expiredAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_Reference(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,fileId: null == fileId ? _self.fileId : fileId // ignore: cast_nullable_to_non_nullable
|
||||
as String,file: freezed == file ? _self.file : file // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,usage: null == usage ? _self.usage : usage // ignore: cast_nullable_to_non_nullable
|
||||
as String,resourceId: null == resourceId ? _self.resourceId : resourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // 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?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of Reference
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res>? get file {
|
||||
if (_self.file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.file!, (value) {
|
||||
return _then(_self.copyWith(file: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
41
lib/models/reference.g.dart
Normal file
41
lib/models/reference.g.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'reference.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference(
|
||||
id: json['id'] as String,
|
||||
fileId: json['file_id'] as String,
|
||||
file:
|
||||
json['file'] == null
|
||||
? null
|
||||
: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
|
||||
usage: json['usage'] as String,
|
||||
resourceId: json['resource_id'] as String,
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
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> _$ReferenceToJson(_Reference instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'file_id': instance.fileId,
|
||||
'file': instance.file?.toJson(),
|
||||
'usage': instance.usage,
|
||||
'resource_id': instance.resourceId,
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/account.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/models/poll.dart";
|
||||
@@ -20,6 +21,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
import "package:island/screens/account/profile.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@@ -45,6 +47,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
bool _isUpdatingState = false;
|
||||
DateTime? _lastPauseTime;
|
||||
|
||||
late final Future<SnAccount?> Function(String) _fetchAccount;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
_roomId = roomId;
|
||||
@@ -53,6 +57,15 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||
|
||||
// Initialize fetch account method for corrupted data recovery
|
||||
_fetchAccount = (String accountId) async {
|
||||
try {
|
||||
return await ref.watch(accountProvider(accountId).future);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (room == null) {
|
||||
throw Exception('Room not found');
|
||||
}
|
||||
@@ -133,6 +146,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_roomId,
|
||||
searchQuery,
|
||||
withAttachments: withAttachments,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
} else {
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
@@ -140,8 +154,16 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
dbMessages = await Future.wait(
|
||||
chatMessagesFromDb
|
||||
.map(
|
||||
(msg) => _database.companionToMessage(
|
||||
msg,
|
||||
fetchAccount: _fetchAccount,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||
@@ -202,8 +224,14 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
final dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
final dbMessages = await Future.wait(
|
||||
chatMessagesFromDb
|
||||
.map(
|
||||
(msg) =>
|
||||
_database.companionToMessage(msg, fetchAccount: _fetchAccount),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
@@ -272,6 +300,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
for (final message in messages) {
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
if (message.sender != null) {
|
||||
await _database.saveMember(message.sender!); // Save/update member data
|
||||
}
|
||||
if (message.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||
@@ -300,7 +331,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final lastMessage =
|
||||
dbMessages.isEmpty
|
||||
? null
|
||||
: _database.companionToMessage(dbMessages.first);
|
||||
: await _database.companionToMessage(
|
||||
dbMessages.first,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
|
||||
if (lastMessage == null) {
|
||||
talker.log('No local messages, fetching from network');
|
||||
@@ -468,6 +502,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
_fileUploadProgress[localMessage.id] = {};
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
await _database.saveMember(mockMessage.sender);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||
@@ -888,7 +923,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
await (_database.select(_database.chatMessages)
|
||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||
if (localMessage != null) {
|
||||
return _database.companionToMessage(localMessage);
|
||||
return _database.companionToMessage(
|
||||
localMessage,
|
||||
fetchAccount: _fetchAccount,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
|
||||
@@ -11,12 +11,36 @@ part 'file_list.g.dart';
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String _currentPath = '/';
|
||||
String? _poolId;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPath(String path) {
|
||||
_currentPath = path;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@@ -26,9 +50,25 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParameters = <String, String>{'path': _currentPath};
|
||||
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/browse',
|
||||
queryParameters: {'path': _currentPath},
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<String> folders =
|
||||
@@ -58,6 +98,37 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
@riverpod
|
||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String? _poolId;
|
||||
bool _recycled = false;
|
||||
String? _query;
|
||||
String? _order;
|
||||
bool _orderDesc = false;
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setRecycled(bool recycled) {
|
||||
_recycled = recycled;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setQuery(String? query) {
|
||||
_query = query;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrder(String? order) {
|
||||
_order = order;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setOrderDesc(bool orderDesc) {
|
||||
_orderDesc = orderDesc;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@@ -70,9 +141,32 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||
const take = 50; // Default page size
|
||||
|
||||
final queryParameters = <String, String>{
|
||||
'take': take.toString(),
|
||||
'offset': offset.toString(),
|
||||
};
|
||||
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
if (_recycled) {
|
||||
queryParameters['recycled'] = _recycled.toString();
|
||||
}
|
||||
|
||||
if (_query != null) {
|
||||
queryParameters['query'] = _query!;
|
||||
}
|
||||
|
||||
if (_order != null) {
|
||||
queryParameters['order'] = _order!;
|
||||
}
|
||||
|
||||
queryParameters['orderDesc'] = _orderDesc.toString();
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/unindexed',
|
||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||
|
||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
||||
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
@@ -66,7 +66,7 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
typedef _$CloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||
String _$unindexedFileListNotifierHash() =>
|
||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
||||
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||
|
||||
/// See also [UnindexedFileListNotifier].
|
||||
@ProviderFor(UnindexedFileListNotifier)
|
||||
|
||||
16
lib/pods/file_references.dart
Normal file
16
lib/pods/file_references.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/reference.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
part 'file_references.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Reference>> fileReferences(Ref ref, String fileId) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/files/$fileId/references');
|
||||
final list = response.data as List<dynamic>;
|
||||
return list
|
||||
.map((json) => Reference.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
153
lib/pods/file_references.g.dart
Normal file
153
lib/pods/file_references.g.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_references.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$fileReferencesHash() => r'464562fbdc9452d8a5ffbd2d9d9343cdb43f1876';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [fileReferences].
|
||||
@ProviderFor(fileReferences)
|
||||
const fileReferencesProvider = FileReferencesFamily();
|
||||
|
||||
/// See also [fileReferences].
|
||||
class FileReferencesFamily extends Family<AsyncValue<List<Reference>>> {
|
||||
/// See also [fileReferences].
|
||||
const FileReferencesFamily();
|
||||
|
||||
/// See also [fileReferences].
|
||||
FileReferencesProvider call(String fileId) {
|
||||
return FileReferencesProvider(fileId);
|
||||
}
|
||||
|
||||
@override
|
||||
FileReferencesProvider getProviderOverride(
|
||||
covariant FileReferencesProvider provider,
|
||||
) {
|
||||
return call(provider.fileId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'fileReferencesProvider';
|
||||
}
|
||||
|
||||
/// See also [fileReferences].
|
||||
class FileReferencesProvider
|
||||
extends AutoDisposeFutureProvider<List<Reference>> {
|
||||
/// See also [fileReferences].
|
||||
FileReferencesProvider(String fileId)
|
||||
: this._internal(
|
||||
(ref) => fileReferences(ref as FileReferencesRef, fileId),
|
||||
from: fileReferencesProvider,
|
||||
name: r'fileReferencesProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$fileReferencesHash,
|
||||
dependencies: FileReferencesFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
FileReferencesFamily._allTransitiveDependencies,
|
||||
fileId: fileId,
|
||||
);
|
||||
|
||||
FileReferencesProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.fileId,
|
||||
}) : super.internal();
|
||||
|
||||
final String fileId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<Reference>> Function(FileReferencesRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: FileReferencesProvider._internal(
|
||||
(ref) => create(ref as FileReferencesRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
fileId: fileId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<Reference>> createElement() {
|
||||
return _FileReferencesProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is FileReferencesProvider && other.fileId == fileId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, fileId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin FileReferencesRef on AutoDisposeFutureProviderRef<List<Reference>> {
|
||||
/// The parameter `fileId` of this provider.
|
||||
String get fileId;
|
||||
}
|
||||
|
||||
class _FileReferencesProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<Reference>>
|
||||
with FileReferencesRef {
|
||||
_FileReferencesProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get fileId => (origin as FileReferencesProvider).fileId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void updateDownloadProgress(
|
||||
String taskId,
|
||||
int downloadedBytes,
|
||||
int totalBytes,
|
||||
) {
|
||||
state =
|
||||
state.map((task) {
|
||||
if (task.taskId == taskId) {
|
||||
return task.copyWith(
|
||||
fileSize: totalBytes,
|
||||
uploadedBytes: downloadedBytes,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return task;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void removeTask(String taskId) {
|
||||
state = state.where((task) => task.taskId != taskId).toList();
|
||||
}
|
||||
@@ -275,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
void clearAllTasks() {
|
||||
state = [];
|
||||
}
|
||||
|
||||
DriveTask? getTask(String taskId) {
|
||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||
}
|
||||
@@ -291,6 +313,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
||||
.toList();
|
||||
}
|
||||
|
||||
String addLocalDownloadTask(SnCloudFile item) {
|
||||
final taskId =
|
||||
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||
final task = DriveTask(
|
||||
id: taskId,
|
||||
taskId: taskId,
|
||||
fileName: item.name,
|
||||
contentType: item.mimeType ?? '',
|
||||
fileSize: 0,
|
||||
uploadedBytes: 0,
|
||||
totalChunks: 1,
|
||||
uploadedChunks: 0,
|
||||
status: DriveTaskStatus.inProgress,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
type: 'FileDownload',
|
||||
);
|
||||
state = [...state, task];
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_websocketSubscription?.cancel();
|
||||
|
||||
@@ -170,6 +170,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
builder: (context, state) => const AboutScreen(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
name: 'fileDetail',
|
||||
path: '/files/:id',
|
||||
builder: (context, state) {
|
||||
// For now, we'll need to pass the file object through extra
|
||||
// This will be updated when we modify the file list navigation
|
||||
final file = state.extra as SnCloudFile?;
|
||||
if (file != null) {
|
||||
return FileDetailScreen(item: file);
|
||||
}
|
||||
// Fallback - this shouldn't happen in normal flow
|
||||
Navigator.of(context).pop();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
// Main tabs with TabsScreen shell
|
||||
ShellRoute(
|
||||
navigatorKey: _tabsShellKey,
|
||||
@@ -427,23 +443,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
name: 'files',
|
||||
path: '/files',
|
||||
builder: (context, state) => const FileListScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'fileDetail',
|
||||
path: ':id',
|
||||
builder: (context, state) {
|
||||
// For now, we'll need to pass the file object through extra
|
||||
// This will be updated when we modify the file list navigation
|
||||
final file = state.extra as SnCloudFile?;
|
||||
if (file != null) {
|
||||
return FileDetailScreen(item: file);
|
||||
}
|
||||
// Fallback - this shouldn't happen in normal flow
|
||||
Navigator.of(context).pop();
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// SN-chan tab
|
||||
|
||||
@@ -9,6 +9,7 @@ class CaptchaScreen extends ConsumerWidget {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class CaptchaScreen extends ConsumerStatefulWidget {
|
||||
static Future<String?> show(BuildContext context) {
|
||||
return showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const CaptchaScreen(),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -6,9 +8,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/database.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/chat/chat_summary.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/event_bus.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
@@ -47,6 +53,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
.watch(chatSummaryProvider)
|
||||
.whenData((summaries) => summaries[room.id]);
|
||||
|
||||
var validMembers = room.members ?? [];
|
||||
if (validMembers.isNotEmpty) {
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
if (userInfo.value != null) {
|
||||
validMembers =
|
||||
validMembers
|
||||
.where((e) => e.accountId != userInfo.value!.id)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildSubtitle() {
|
||||
if (subtitle != null) return subtitle!;
|
||||
|
||||
@@ -55,7 +72,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
if (data == null) {
|
||||
return isDirect && room.description == null
|
||||
? Text(
|
||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
||||
@@ -111,7 +128,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
(_, _) =>
|
||||
isDirect && room.description == null
|
||||
? Text(
|
||||
room.members!.map((e) => '@${e.account.name}').join(', '),
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
@@ -121,6 +138,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String titleText;
|
||||
if (isDirect && room.name == null) {
|
||||
if (room.members?.isNotEmpty ?? false) {
|
||||
titleText = validMembers.map((e) => e.account.nick).join(', ');
|
||||
} else {
|
||||
titleText = 'Direct Message';
|
||||
}
|
||||
} else {
|
||||
titleText = room.name ?? '';
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
leading: Badge(
|
||||
isLabelVisible: summary.when(
|
||||
@@ -132,7 +160,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
(isDirect && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
@@ -140,11 +168,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||
),
|
||||
title: Text(
|
||||
(isDirect && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name ?? '',
|
||||
),
|
||||
title: Text(titleText),
|
||||
subtitle: buildSubtitle(),
|
||||
trailing: trailing, // Add this line
|
||||
onTap: () async {
|
||||
@@ -162,12 +186,92 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
|
||||
@riverpod
|
||||
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||
final db = ref.watch(databaseProvider);
|
||||
|
||||
try {
|
||||
final localRoomsData = await db.select(db.chatRooms).get();
|
||||
if (localRoomsData.isNotEmpty) {
|
||||
final localRooms = await Future.wait(
|
||||
localRoomsData.map((row) async {
|
||||
final membersRows =
|
||||
await (db.select(db.chatMembers)
|
||||
..where((m) => m.chatRoomId.equals(row.id))).get();
|
||||
final members =
|
||||
membersRows.map((mRow) {
|
||||
final account = SnAccount.fromJson(mRow.account);
|
||||
SnAccountStatus? status;
|
||||
if (mRow.status != null) {
|
||||
status = SnAccountStatus.fromJson(jsonDecode(mRow.status!));
|
||||
}
|
||||
return SnChatMember(
|
||||
id: mRow.id,
|
||||
chatRoomId: mRow.chatRoomId,
|
||||
accountId: mRow.accountId,
|
||||
account: account,
|
||||
nick: mRow.nick,
|
||||
role: mRow.role,
|
||||
notify: mRow.notify,
|
||||
joinedAt: mRow.joinedAt,
|
||||
breakUntil: mRow.breakUntil,
|
||||
timeoutUntil: mRow.timeoutUntil,
|
||||
isBot: mRow.isBot,
|
||||
status: status,
|
||||
lastTyped: mRow.lastTyped,
|
||||
createdAt: mRow.createdAt,
|
||||
updatedAt: mRow.updatedAt,
|
||||
deletedAt: mRow.deletedAt,
|
||||
chatRoom: null,
|
||||
);
|
||||
}).toList();
|
||||
return SnChatRoom(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
type: row.type,
|
||||
isPublic: row.isPublic!,
|
||||
isCommunity: row.isCommunity!,
|
||||
picture:
|
||||
row.picture != null ? SnCloudFile.fromJson(row.picture!) : null,
|
||||
background:
|
||||
row.background != null
|
||||
? SnCloudFile.fromJson(row.background!)
|
||||
: null,
|
||||
realmId: row.realmId,
|
||||
realm: null,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
deletedAt: row.deletedAt,
|
||||
members: members,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Background sync
|
||||
Future(() async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.get('/sphere/chat');
|
||||
final remoteRooms =
|
||||
resp.data
|
||||
.map((e) => SnChatRoom.fromJson(e))
|
||||
.cast<SnChatRoom>()
|
||||
.toList();
|
||||
await db.saveChatRooms(remoteRooms);
|
||||
ref.invalidateSelf();
|
||||
} catch (_) {}
|
||||
}).ignore();
|
||||
|
||||
return localRooms;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Fallback to API
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/sphere/chat');
|
||||
return resp.data
|
||||
.map((e) => SnChatRoom.fromJson(e))
|
||||
.cast<SnChatRoom>()
|
||||
.toList();
|
||||
final rooms =
|
||||
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||
await db.saveChatRooms(rooms);
|
||||
return rooms;
|
||||
}
|
||||
|
||||
class ChatListBodyWidget extends HookConsumerWidget {
|
||||
|
||||
@@ -148,9 +148,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
final inputKey = useMemoized(() => GlobalKey());
|
||||
final inputHeight = useState<double>(80.0);
|
||||
|
||||
// Track previous height for smooth animations
|
||||
final previousInputHeight = usePrevious<double>(inputHeight.value);
|
||||
|
||||
// Periodic height measurement for dynamic sizing
|
||||
useEffect(() {
|
||||
final timer = Timer.periodic(const Duration(milliseconds: 50), (_) {
|
||||
@@ -624,428 +621,179 @@ class ChatRoomScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
previousInputHeight != null && previousInputHeight != inputHeight.value
|
||||
? TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(
|
||||
begin: previousInputHeight,
|
||||
end: inputHeight.value,
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
builder:
|
||||
(context, height, child) => SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom + 8 + height,
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(
|
||||
messageKeyPrefix.length,
|
||||
);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
// Return null for invalid indices to let SuperListView handle it properly
|
||||
return index >= 0 ? index : null;
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1
|
||||
? messageList[index + 1]
|
||||
: null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
Widget chatMessageListWidget(
|
||||
List<LocalChatMessage> messageList,
|
||||
) => AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value,
|
||||
),
|
||||
child: SuperListView.builder(
|
||||
listController: listController,
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
return index >= 0 ? index : null;
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
// Use a stable animation key that doesn't change during message lifecycle
|
||||
final key = Key(
|
||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
||||
);
|
||||
final key = Key('$messageKeyPrefix${message.nonce ?? message.id}');
|
||||
|
||||
final messageWidget = chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => GestureDetector(
|
||||
onLongPress: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isSelectionMode.value) {
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
selectedMessages.value.contains(message.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.3)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
MessageItem(
|
||||
key:
|
||||
settings.disableAnimation
|
||||
? key
|
||||
: null,
|
||||
message: message,
|
||||
isCurrentUser:
|
||||
identity?.id == message.senderId,
|
||||
onAction:
|
||||
isSelectionMode.value
|
||||
? null
|
||||
: (action) {
|
||||
switch (action) {
|
||||
case MessageItemAction.delete:
|
||||
messagesNotifier
|
||||
.deleteMessage(
|
||||
message.id,
|
||||
);
|
||||
case MessageItemAction.edit:
|
||||
messageEditingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
messageController.text =
|
||||
messageEditingTo
|
||||
.value
|
||||
?.content ??
|
||||
'';
|
||||
attachments.value =
|
||||
messageEditingTo
|
||||
.value!
|
||||
.attachments
|
||||
.map(
|
||||
(e) =>
|
||||
UniversalFile.fromAttachment(
|
||||
e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
case MessageItemAction
|
||||
.forward:
|
||||
messageForwardingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
case MessageItemAction.reply:
|
||||
messageReplyingTo.value =
|
||||
message
|
||||
.toRemoteMessage();
|
||||
case MessageItemAction.resend:
|
||||
messagesNotifier
|
||||
.retryMessage(
|
||||
message.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onJump: (messageId) {
|
||||
scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
final messageWidget = chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => GestureDetector(
|
||||
onLongPress: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isSelectionMode.value) {
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
selectedMessages.value.contains(message.id)
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer.withOpacity(0.3)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
MessageItem(
|
||||
key: settings.disableAnimation ? key : null,
|
||||
message: message,
|
||||
isCurrentUser: identity?.id == message.senderId,
|
||||
onAction:
|
||||
isSelectionMode.value
|
||||
? null
|
||||
: (action) {
|
||||
switch (action) {
|
||||
case MessageItemAction.delete:
|
||||
messagesNotifier.deleteMessage(
|
||||
message.id,
|
||||
);
|
||||
},
|
||||
progress:
|
||||
attachmentProgress.value[message.id],
|
||||
showAvatar: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
isSelected: selectedMessages.value
|
||||
.contains(message.id),
|
||||
onToggleSelection: toggleMessageSelection,
|
||||
onEnterSelectionMode: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (selectedMessages.value.contains(
|
||||
message.id,
|
||||
))
|
||||
...([
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
case MessageItemAction.edit:
|
||||
messageEditingTo.value =
|
||||
message.toRemoteMessage();
|
||||
messageController.text =
|
||||
messageEditingTo.value?.content ??
|
||||
'';
|
||||
attachments.value =
|
||||
messageEditingTo.value!.attachments
|
||||
.map(
|
||||
(e) =>
|
||||
UniversalFile.fromAttachment(
|
||||
e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
case MessageItemAction.forward:
|
||||
messageForwardingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.reply:
|
||||
messageReplyingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.resend:
|
||||
messagesNotifier.retryMessage(
|
||||
message.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onJump:
|
||||
(messageId) => scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
onAction: null,
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
onJump: (_) {},
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return settings.disableAnimation
|
||||
? messageWidget
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: key,
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: Duration(
|
||||
milliseconds: 400 + (index % 5) * 50,
|
||||
), // Staggered delay
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, animationValue, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
20 * (1 - animationValue),
|
||||
), // Slide up from bottom
|
||||
child: Opacity(
|
||||
opacity: animationValue,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
8 +
|
||||
inputHeight.value,
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
if (key is! ValueKey<String>) return null;
|
||||
final messageId = key.value.substring(messageKeyPrefix.length);
|
||||
final index = messageList.indexWhere(
|
||||
(m) => (m.nonce ?? m.id) == messageId,
|
||||
);
|
||||
// Return null for invalid indices to let SuperListView handle it properly
|
||||
return index >= 0 ? index : null;
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1
|
||||
? messageList[index + 1]
|
||||
: null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
// Use a stable animation key that doesn't change during message lifecycle
|
||||
final key = Key(
|
||||
'$messageKeyPrefix${message.nonce ?? message.id}',
|
||||
);
|
||||
|
||||
final messageWidget = chatIdentity.when(
|
||||
skipError: true,
|
||||
data:
|
||||
(identity) => GestureDetector(
|
||||
onLongPress: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isSelectionMode.value) {
|
||||
toggleMessageSelection(message.id);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color:
|
||||
selectedMessages.value.contains(message.id)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.3)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
MessageItem(
|
||||
key: settings.disableAnimation ? key : null,
|
||||
message: message,
|
||||
isCurrentUser: identity?.id == message.senderId,
|
||||
onAction:
|
||||
isSelectionMode.value
|
||||
? null
|
||||
: (action) {
|
||||
switch (action) {
|
||||
case MessageItemAction.delete:
|
||||
messagesNotifier.deleteMessage(
|
||||
message.id,
|
||||
);
|
||||
case MessageItemAction.edit:
|
||||
messageEditingTo.value =
|
||||
message.toRemoteMessage();
|
||||
messageController.text =
|
||||
messageEditingTo
|
||||
.value
|
||||
?.content ??
|
||||
'';
|
||||
attachments.value =
|
||||
messageEditingTo
|
||||
.value!
|
||||
.attachments
|
||||
.map(
|
||||
(e) =>
|
||||
UniversalFile.fromAttachment(
|
||||
e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
case MessageItemAction.forward:
|
||||
messageForwardingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.reply:
|
||||
messageReplyingTo.value =
|
||||
message.toRemoteMessage();
|
||||
case MessageItemAction.resend:
|
||||
messagesNotifier.retryMessage(
|
||||
message.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
onJump: (messageId) {
|
||||
scrollToMessage(
|
||||
messageId: messageId,
|
||||
messageList: messageList,
|
||||
messagesNotifier: messagesNotifier,
|
||||
listController: listController,
|
||||
scrollController: scrollController,
|
||||
ref: ref,
|
||||
);
|
||||
},
|
||||
progress: attachmentProgress.value[message.id],
|
||||
showAvatar: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
isSelected: selectedMessages.value.contains(
|
||||
message.id,
|
||||
),
|
||||
onToggleSelection: toggleMessageSelection,
|
||||
onEnterSelectionMode: () {
|
||||
if (!isSelectionMode.value) {
|
||||
toggleSelectionMode();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (selectedMessages.value.contains(message.id))
|
||||
...([
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
progress: attachmentProgress.value[message.id],
|
||||
showAvatar: isLastInGroup,
|
||||
isSelectionMode: isSelectionMode.value,
|
||||
isSelected: selectedMessages.value.contains(
|
||||
message.id,
|
||||
),
|
||||
onToggleSelection: toggleMessageSelection,
|
||||
onEnterSelectionMode: () {
|
||||
if (!isSelectionMode.value) toggleSelectionMode();
|
||||
},
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
onAction: null,
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
onJump: (_) {},
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
if (selectedMessages.value.contains(message.id))
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false,
|
||||
onAction: null,
|
||||
progress: null,
|
||||
showAvatar: false,
|
||||
onJump: (_) {},
|
||||
),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
return settings.disableAnimation
|
||||
? messageWidget
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: key,
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: Duration(
|
||||
milliseconds: 400 + (index % 5) * 50,
|
||||
), // Staggered delay
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, animationValue, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
20 * (1 - animationValue),
|
||||
), // Slide up from bottom
|
||||
child: Opacity(opacity: animationValue, child: child),
|
||||
);
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
return settings.disableAnimation
|
||||
? messageWidget
|
||||
: TweenAnimationBuilder<double>(
|
||||
key: key,
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 400 + (index % 5) * 50),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder:
|
||||
(context, animationValue, child) => Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - animationValue)),
|
||||
child: Opacity(opacity: animationValue, child: child),
|
||||
),
|
||||
child: messageWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
|
||||
@@ -6,17 +6,24 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/file_references.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/upload_tasks.dart';
|
||||
import 'package:island/models/drive_task.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/file_viewer_contents.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class FileDetailScreen extends HookConsumerWidget {
|
||||
final SnCloudFile item;
|
||||
@@ -76,7 +83,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
}, [animationController]);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: true,
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
@@ -86,26 +93,47 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||
),
|
||||
body: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Row(
|
||||
children: [
|
||||
// Main content area
|
||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
||||
// Animated drawer panel
|
||||
if (isWide)
|
||||
SizedBox(
|
||||
height: double.infinity,
|
||||
width: animation.value * 400, // Max width of 400px
|
||||
child: Container(
|
||||
child:
|
||||
animation.value > 0.1
|
||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
||||
: const SizedBox.shrink(),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Main content area - resizes with animation
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: constraints.maxWidth - animation.value * 400,
|
||||
child: _buildContent(context, ref, serverUrl),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Animated drawer panel - overlays
|
||||
if (isWide)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 400,
|
||||
child: Transform.translate(
|
||||
offset: Offset((1 - animation.value) * 400, 0),
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
elevation: 8,
|
||||
child: FileInfoSheet(
|
||||
item: item,
|
||||
onClose: showInfoSheet,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -144,6 +172,24 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add references button
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: Icon(Icons.link),
|
||||
onPressed:
|
||||
() => showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'File References',
|
||||
child: ReferencesList(fileId: item.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Always add info button
|
||||
actions.add(
|
||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||
@@ -187,6 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> _downloadFile(WidgetRef ref) async {
|
||||
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
@@ -202,14 +250,26 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
onReceiveProgress: (count, total) {
|
||||
if (total > 0) {
|
||||
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
taskNotifier.updateTaskStatus(
|
||||
taskId,
|
||||
DriveTaskStatus.failed,
|
||||
errorMessage: e.toString(),
|
||||
);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
@@ -229,3 +289,54 @@ class FileDetailScreen extends HookConsumerWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ReferencesList extends ConsumerWidget {
|
||||
const ReferencesList({super.key, required this.fileId});
|
||||
|
||||
final String fileId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asyncReferences = ref.watch(fileReferencesProvider(fileId));
|
||||
|
||||
return asyncReferences.when(
|
||||
data:
|
||||
(references) => ListView.builder(
|
||||
itemCount: references.length,
|
||||
itemBuilder: (context, index) {
|
||||
final reference = references[index];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
reference.usage,
|
||||
style: GoogleFonts.robotoMono(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
reference.id,
|
||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(reference.createdAt.formatRelative(context)),
|
||||
const VerticalDivider(width: 1, thickness: 1).height(12),
|
||||
Text(reference.createdAt.formatSystem()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => Center(child: Text('Error loading references: $error')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
import 'package:island/pods/file_list.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
@@ -23,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
// Path navigation state
|
||||
final currentPath = useState<String>('/');
|
||||
final mode = useState<FileListMode>(FileListMode.normal);
|
||||
final selectedPool = useState<SnFilePool?>(null);
|
||||
|
||||
final usageAsync = ref.watch(billingUsageProvider);
|
||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||
@@ -32,7 +35,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text('Files'),
|
||||
title: Text('files').tr(),
|
||||
leading: const PageBackButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -55,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
|
||||
usage: usage,
|
||||
quota: quota,
|
||||
currentPath: currentPath,
|
||||
selectedPool: selectedPool,
|
||||
onPickAndUpload:
|
||||
() => _pickAndUploadFile(ref, currentPath.value),
|
||||
() => _pickAndUploadFile(
|
||||
ref,
|
||||
currentPath.value,
|
||||
selectedPool.value?.id,
|
||||
),
|
||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||
mode: mode,
|
||||
viewMode: viewMode,
|
||||
@@ -70,7 +78,11 @@ class FileListScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
||||
Future<void> _pickAndUploadFile(
|
||||
WidgetRef ref,
|
||||
String currentPath,
|
||||
String? poolId,
|
||||
) async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: true,
|
||||
@@ -92,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
|
||||
fileData: universalFile,
|
||||
ref: ref,
|
||||
path: currentPath,
|
||||
poolId: poolId,
|
||||
onProgress: (progress, _) {
|
||||
// Progress is handled by the upload tasks system
|
||||
if (progress != null) {
|
||||
|
||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
Navigator.of(context).pop();
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
||||
@@ -39,6 +39,7 @@ class AccountName extends StatelessWidget {
|
||||
final String? textOverride;
|
||||
final bool ignorePermissions;
|
||||
final bool hideVerificationMark;
|
||||
final bool hideOverlay;
|
||||
const AccountName({
|
||||
super.key,
|
||||
required this.account,
|
||||
@@ -46,6 +47,7 @@ class AccountName extends StatelessWidget {
|
||||
this.textOverride,
|
||||
this.ignorePermissions = false,
|
||||
this.hideVerificationMark = false,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
Alignment _parseGradientDirection(String direction) {
|
||||
@@ -189,20 +191,33 @@ class AccountName extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (account.perkSubscription != null)
|
||||
StellarMembershipMark(membership: account.perkSubscription!),
|
||||
StellarMembershipMark(
|
||||
membership: account.perkSubscription!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.profile.verification != null &&
|
||||
!hideVerificationMark)
|
||||
VerificationMark(mark: account.profile.verification!),
|
||||
if (account.automatedId != null)
|
||||
Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
VerificationMark(
|
||||
mark: account.profile.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.automatedId != null)
|
||||
hideOverlay
|
||||
? Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
)
|
||||
: Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -226,26 +241,39 @@ class AccountName extends StatelessWidget {
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
account.nick,
|
||||
textOverride ?? account.nick,
|
||||
style: nameStyle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (account.perkSubscription != null)
|
||||
StellarMembershipMark(membership: account.perkSubscription!),
|
||||
if (account.profile.verification != null)
|
||||
VerificationMark(mark: account.profile.verification!),
|
||||
if (account.automatedId != null)
|
||||
Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
StellarMembershipMark(
|
||||
membership: account.perkSubscription!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.profile.verification != null)
|
||||
VerificationMark(
|
||||
mark: account.profile.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (account.automatedId != null)
|
||||
hideOverlay
|
||||
? Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
)
|
||||
: Tooltip(
|
||||
message: 'accountAutomated'.tr(),
|
||||
child: Icon(
|
||||
Symbols.smart_toy,
|
||||
size: 16,
|
||||
color: nameStyle.color,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -253,39 +281,53 @@ class AccountName extends StatelessWidget {
|
||||
|
||||
class VerificationMark extends StatelessWidget {
|
||||
final SnVerificationMark mark;
|
||||
const VerificationMark({super.key, required this.mark});
|
||||
final bool hideOverlay;
|
||||
const VerificationMark({
|
||||
super.key,
|
||||
required this.mark,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: mark.title ?? 'No title',
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: mark.description ?? 'descriptionNone'.tr(),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: Icon(
|
||||
mark.type == 4
|
||||
? Symbols.play_circle
|
||||
: mark.type == 0
|
||||
? Symbols.build_circle
|
||||
: Symbols.verified,
|
||||
size: 16,
|
||||
color: kVerificationMarkColors[mark.type],
|
||||
fill: 1,
|
||||
),
|
||||
final icon = Icon(
|
||||
mark.type == 4
|
||||
? Symbols.play_circle
|
||||
: mark.type == 0
|
||||
? Symbols.build_circle
|
||||
: Symbols.verified,
|
||||
size: 16,
|
||||
color: kVerificationMarkColors[mark.type],
|
||||
fill: 1,
|
||||
);
|
||||
|
||||
return hideOverlay
|
||||
? icon
|
||||
: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: mark.title ?? 'No title',
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: mark.description ?? 'descriptionNone'.tr(),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StellarMembershipMark extends StatelessWidget {
|
||||
final SnWalletSubscriptionRef membership;
|
||||
const StellarMembershipMark({super.key, required this.membership});
|
||||
final bool hideOverlay;
|
||||
const StellarMembershipMark({
|
||||
super.key,
|
||||
required this.membership,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
String _getMembershipTierName(String identifier) {
|
||||
switch (identifier) {
|
||||
@@ -321,20 +363,24 @@ class StellarMembershipMark extends StatelessWidget {
|
||||
final tierColor = _getMembershipTierColor(membership.identifier);
|
||||
final tierIcon = Symbols.kid_star;
|
||||
|
||||
return Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: 'stellarMembership'.tr(),
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'currentMembershipMember'.tr(args: [tierName]),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
final icon = Icon(tierIcon, size: 16, color: tierColor, fill: 1);
|
||||
|
||||
return hideOverlay
|
||||
? icon
|
||||
: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: 'stellarMembership'.tr(),
|
||||
children: [
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'currentMembershipMember'.tr(args: [tierName]),
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: Icon(tierIcon, size: 16, color: tierColor, fill: 1),
|
||||
);
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ class CloudFileList extends HookConsumerWidget {
|
||||
}
|
||||
if (files.length == 1) {
|
||||
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
||||
final ratio = files.first.fileMeta?['ratio'] as num?;
|
||||
final widgetItem = ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: _CloudFileListEntry(
|
||||
@@ -242,7 +243,13 @@ class CloudFileList extends HookConsumerWidget {
|
||||
minWidth: minWidth ?? 0,
|
||||
maxWidth: files.length == 1 ? maxWidth : double.infinity,
|
||||
),
|
||||
child: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
||||
child:
|
||||
(ratio == null && isImage)
|
||||
? IntrinsicWidth(child: IntrinsicHeight(child: widgetItem))
|
||||
: AspectRatio(
|
||||
aspectRatio: ratio?.toDouble() ?? 1,
|
||||
child: widgetItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -408,8 +415,6 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||
|
||||
final ratio = meta['ratio'] as num?;
|
||||
|
||||
final fit = BoxFit.cover;
|
||||
|
||||
Widget bg = const SizedBox.shrink();
|
||||
@@ -448,9 +453,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
fit: fit,
|
||||
useInternalGate: false,
|
||||
))
|
||||
: IntrinsicWidth(
|
||||
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
||||
);
|
||||
: const SizedBox.shrink();
|
||||
|
||||
Widget overlays;
|
||||
if (lockedByDS) {
|
||||
@@ -481,7 +484,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
||||
onTap?.call();
|
||||
}
|
||||
},
|
||||
child: AspectRatio(aspectRatio: ratio?.toDouble() ?? 1, child: content),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
@@ -171,6 +172,24 @@ class CloudFileLightbox extends HookConsumerWidget {
|
||||
),
|
||||
onPressed: showInfoSheet,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final router = GoRouter.of(context);
|
||||
Navigator.of(context).pop(context);
|
||||
Future(() {
|
||||
router.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.more_horiz,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/widgets/data_saving_gate.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
|
||||
import 'file_viewer_contents.dart';
|
||||
import 'image.dart';
|
||||
@@ -66,34 +59,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (item.mimeType == 'application/pdf') {
|
||||
Future<void> downloadFile() async {
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'pdf';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
@@ -166,30 +131,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.download,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: downloadFile,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.info,
|
||||
Symbols.more_horiz,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -201,34 +156,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
if (item.mimeType?.startsWith('text/') == true) {
|
||||
Future<void> downloadFile() async {
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'txt';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
@@ -304,30 +231,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.download,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: downloadFile,
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.info,
|
||||
Symbols.more_horiz,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -356,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
'audio' => AudioFileContent(item: item, uri: uri),
|
||||
_ => Builder(
|
||||
builder: (context) {
|
||||
Future<void> downloadFile() async {
|
||||
try {
|
||||
showSnackBar('Downloading file...');
|
||||
|
||||
final client = ref.read(apiClientProvider);
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
var extName = extension(item.name).trim();
|
||||
if (extName.isEmpty) {
|
||||
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
|
||||
}
|
||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||
|
||||
await client.download(
|
||||
'/drive/files/${item.id}',
|
||||
filePath,
|
||||
queryParameters: {'original': true},
|
||||
);
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||
file: File(filePath),
|
||||
);
|
||||
showSnackBar('File saved to downloads');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -422,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: downloadFile,
|
||||
icon: const Icon(Symbols.download),
|
||||
label: Text('download').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
context.pushNamed(
|
||||
'fileDetail',
|
||||
pathParameters: {'id': item.id},
|
||||
extra: item,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.info),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -28,8 +30,64 @@ class PdfFileContent extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
||||
return pdfViewer;
|
||||
final fileFuture = useMemoized(
|
||||
() => DefaultCacheManager().getSingleFile(uri),
|
||||
[uri],
|
||||
);
|
||||
|
||||
final pdfController = useMemoized(() => PdfViewerController(), []);
|
||||
|
||||
final shadow = [
|
||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||
];
|
||||
|
||||
return FutureBuilder<File>(
|
||||
future: fileFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('Error loading PDF: ${snapshot.error}'));
|
||||
} else if (snapshot.hasData) {
|
||||
return Stack(
|
||||
children: [
|
||||
SfPdfViewer.file(snapshot.data!, controller: pdfController),
|
||||
// Controls overlay
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.remove,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
pdfController.zoomLevel = pdfController.zoomLevel * 0.9;
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
shadows: shadow,
|
||||
),
|
||||
onPressed: () {
|
||||
pdfController.zoomLevel = pdfController.zoomLevel * 1.1;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const Center(child: Text('No PDF data'));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,19 +147,39 @@ class ImageFileContent extends HookConsumerWidget {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
child: Listener(
|
||||
onPointerSignal: (pointerSignal) {
|
||||
try {
|
||||
// Handle mouse wheel zoom - cast to dynamic to access scrollDelta
|
||||
final delta =
|
||||
(pointerSignal as dynamic).scrollDelta.dy as double?;
|
||||
if (delta != null && delta != 0) {
|
||||
final currentScale = photoViewController.scale ?? 1.0;
|
||||
// Adjust scale based on scroll direction (invert for natural zoom)
|
||||
final newScale =
|
||||
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
|
||||
// Clamp scale to reasonable bounds
|
||||
final clampedScale = newScale.clamp(0.1, 10.0);
|
||||
photoViewController.scale = clampedScale;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore non-scroll events
|
||||
}
|
||||
},
|
||||
child: PhotoView(
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
),
|
||||
controller: photoViewController,
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
original: showOriginal.value,
|
||||
),
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
controller: photoViewController,
|
||||
imageProvider: CloudImageWidget.provider(
|
||||
fileId: item.id,
|
||||
serverUrl: ref.watch(serverUrlProvider),
|
||||
original: showOriginal.value,
|
||||
),
|
||||
customSize: MediaQuery.of(context).size,
|
||||
basePosition: Alignment.center,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
// Controls overlay
|
||||
@@ -245,68 +323,57 @@ class GenericFileContent extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(32),
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.insert_drive_file,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.insert_drive_file,
|
||||
size: 64,
|
||||
const Gap(16),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
formatFileSize(item.size),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: downloadFile,
|
||||
icon: const Icon(Symbols.download),
|
||||
label: Text('download').tr(),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
formatFileSize(item.size),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
const Gap(16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.info),
|
||||
label: Text('info').tr(),
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: downloadFile,
|
||||
icon: const Icon(Symbols.download),
|
||||
label: Text('download'),
|
||||
),
|
||||
const Gap(16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.info),
|
||||
label: Text('info'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
@@ -28,28 +25,12 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
||||
VideoController? _videoController;
|
||||
|
||||
void _openVideo() async {
|
||||
final url = widget.uri;
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
_player = Player();
|
||||
_videoController = VideoController(_player!);
|
||||
|
||||
String? uri;
|
||||
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
||||
if (inCacheInfo == null) {
|
||||
talker.info('[MediaPlayer] Miss cache: $url');
|
||||
final token = ref.watch(tokenProvider)?.token;
|
||||
DefaultCacheManager().downloadFile(
|
||||
url,
|
||||
authHeaders: {'Authorization': 'AtField $token'},
|
||||
);
|
||||
uri = url;
|
||||
} else {
|
||||
uri = inCacheInfo.file.path;
|
||||
talker.info('[MediaPlayer] Hit cache: $url');
|
||||
}
|
||||
|
||||
_player!.open(Media(uri), play: widget.autoplay);
|
||||
_player!.open(Media(widget.uri), play: widget.autoplay);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -29,7 +29,7 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter access token',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -457,7 +457,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
maxLines: 6,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -281,8 +281,8 @@ class ComposeFundSheet extends HookConsumerWidget {
|
||||
// Return the fund that was just created (but not yet paid)
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
Navigator.of(context).pop(fund);
|
||||
}
|
||||
Navigator.of(context).pop(fund);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,10 +327,10 @@ class ComposeFundSheet extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(updatedFund);
|
||||
}
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(updatedFund);
|
||||
} else {
|
||||
isPushing.value = false;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
||||
children: [
|
||||
Gap(renderingPadding.vertical),
|
||||
PostHeader(
|
||||
hideOverlay: true,
|
||||
item: item,
|
||||
isFullPost: isFullPost,
|
||||
isInteractive: false,
|
||||
@@ -73,6 +74,7 @@ class PostItemScreenshot extends ConsumerWidget {
|
||||
isFullPost: isFullPost,
|
||||
isTextSelectable: false,
|
||||
isInteractive: false,
|
||||
hideOverlay: true,
|
||||
),
|
||||
if (isShowReference)
|
||||
ReferencedPostWidget(
|
||||
|
||||
@@ -554,6 +554,7 @@ class PostHeader extends StatelessWidget {
|
||||
final EdgeInsets renderingPadding;
|
||||
final bool isRelativeTime;
|
||||
final bool isCompact;
|
||||
final bool hideOverlay;
|
||||
|
||||
const PostHeader({
|
||||
super.key,
|
||||
@@ -564,6 +565,7 @@ class PostHeader extends StatelessWidget {
|
||||
this.renderingPadding = EdgeInsets.zero,
|
||||
this.isRelativeTime = true,
|
||||
this.isCompact = false,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -606,6 +608,7 @@ class PostHeader extends StatelessWidget {
|
||||
(item.publisher.account != null &&
|
||||
item.publisher.type == 0)
|
||||
? AccountName(
|
||||
hideOverlay: hideOverlay,
|
||||
account: item.publisher.account!,
|
||||
textOverride: item.publisher.nick,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
@@ -618,7 +621,10 @@ class PostHeader extends StatelessWidget {
|
||||
).bold(),
|
||||
),
|
||||
if (item.publisher.verification != null)
|
||||
VerificationMark(mark: item.publisher.verification!),
|
||||
VerificationMark(
|
||||
mark: item.publisher.verification!,
|
||||
hideOverlay: hideOverlay,
|
||||
),
|
||||
if (item.realm == null)
|
||||
Flexible(
|
||||
child:
|
||||
@@ -690,6 +696,7 @@ class PostBody extends ConsumerWidget {
|
||||
final bool isInteractive;
|
||||
final EdgeInsets renderingPadding;
|
||||
final bool isRelativeTime;
|
||||
final bool hideOverlay;
|
||||
|
||||
const PostBody({
|
||||
super.key,
|
||||
@@ -700,6 +707,7 @@ class PostBody extends ConsumerWidget {
|
||||
this.isInteractive = true,
|
||||
this.renderingPadding = EdgeInsets.zero,
|
||||
this.isRelativeTime = true,
|
||||
this.hideOverlay = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -771,27 +779,31 @@ class PostBody extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
if (item.editedAt != null) {
|
||||
final text = Text(
|
||||
'editedAt'.tr(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
),
|
||||
).fontSize(13);
|
||||
|
||||
metadataChildren.add(
|
||||
Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.edit, size: 16),
|
||||
Tooltip(
|
||||
message:
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatSystem()
|
||||
: item.editedAt!.formatRelative(context),
|
||||
child: Text(
|
||||
'editedAt'.tr(
|
||||
args: [
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatRelative(context)
|
||||
: item.editedAt!.formatSystem(),
|
||||
],
|
||||
hideOverlay
|
||||
? text
|
||||
: Tooltip(
|
||||
message:
|
||||
!isFullPost && isRelativeTime
|
||||
? item.editedAt!.formatSystem()
|
||||
: item.editedAt!.formatRelative(context),
|
||||
child: text,
|
||||
),
|
||||
).fontSize(13),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@@ -29,7 +29,49 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
||||
|
||||
final isVisible = activeTasks.isNotEmpty;
|
||||
final isVisibleOverride = useState<bool?>(null);
|
||||
final pendingHide = useState(false);
|
||||
final isExpandedLocal = useState(false);
|
||||
final autoHideTimer = useState<Timer?>(null);
|
||||
|
||||
final allFinished = activeTasks.every(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.completed ||
|
||||
task.status == DriveTaskStatus.failed ||
|
||||
task.status == DriveTaskStatus.cancelled ||
|
||||
task.status == DriveTaskStatus.expired,
|
||||
);
|
||||
|
||||
// Auto-hide timer effect
|
||||
useEffect(() {
|
||||
// Reset pendingHide if there are unfinished tasks
|
||||
final hasUnfinishedTasks = activeTasks.any(
|
||||
(task) =>
|
||||
task.status == DriveTaskStatus.pending ||
|
||||
task.status == DriveTaskStatus.inProgress ||
|
||||
task.status == DriveTaskStatus.paused,
|
||||
);
|
||||
if (hasUnfinishedTasks && pendingHide.value) {
|
||||
pendingHide.value = false;
|
||||
}
|
||||
|
||||
autoHideTimer.value?.cancel();
|
||||
if (allFinished &&
|
||||
activeTasks.isNotEmpty &&
|
||||
!isExpandedLocal.value &&
|
||||
!pendingHide.value) {
|
||||
autoHideTimer.value = Timer(const Duration(seconds: 3), () {
|
||||
pendingHide.value = true;
|
||||
});
|
||||
} else {
|
||||
autoHideTimer.value?.cancel();
|
||||
autoHideTimer.value = null;
|
||||
}
|
||||
return null;
|
||||
}, [allFinished, activeTasks, isExpandedLocal.value, pendingHide.value]);
|
||||
final isVisible =
|
||||
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
|
||||
!pendingHide.value;
|
||||
final slideController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
@@ -63,6 +105,8 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
position: slideAnimation,
|
||||
child: _UploadOverlayContent(
|
||||
activeTasks: activeTasks,
|
||||
isExpanded: isExpandedLocal.value,
|
||||
onExpansionChanged: (expanded) => isExpandedLocal.value = expanded,
|
||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
);
|
||||
@@ -71,12 +115,17 @@ class UploadOverlay extends HookConsumerWidget {
|
||||
|
||||
class _UploadOverlayContent extends HookConsumerWidget {
|
||||
final List<DriveTask> activeTasks;
|
||||
final bool isExpanded;
|
||||
final Function(bool)? onExpansionChanged;
|
||||
|
||||
const _UploadOverlayContent({required this.activeTasks});
|
||||
const _UploadOverlayContent({
|
||||
required this.activeTasks,
|
||||
required this.isExpanded,
|
||||
this.onExpansionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
initialValue: 0.0,
|
||||
@@ -91,15 +140,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (isExpanded.value) {
|
||||
if (isExpanded) {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
return null;
|
||||
}, [isExpanded.value]);
|
||||
}, [isExpanded]);
|
||||
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
final isMobile = !isWideScreen(context);
|
||||
|
||||
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
@@ -108,7 +159,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
right: isMobile ? 16 : 24,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
onTap: () => onExpansionChanged?.call(!isExpanded),
|
||||
child: AnimatedBuilder(
|
||||
animation: animationController,
|
||||
builder: (context, child) {
|
||||
@@ -142,8 +193,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
key: ValueKey(isExpanded.value),
|
||||
isExpanded.value
|
||||
key: ValueKey(isExpanded),
|
||||
isExpanded
|
||||
? Symbols.list_rounded
|
||||
: _getOverallStatusIcon(activeTasks),
|
||||
size: 24,
|
||||
@@ -159,7 +210,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded.value
|
||||
isExpanded
|
||||
? 'uploadTasks'.tr()
|
||||
: _getOverallStatusText(activeTasks),
|
||||
style: Theme.of(context)
|
||||
@@ -169,8 +220,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!isExpanded.value &&
|
||||
activeTasks.isNotEmpty)
|
||||
if (!isExpanded && activeTasks.isNotEmpty)
|
||||
Text(
|
||||
_getOverallProgressText(activeTasks),
|
||||
style: Theme.of(
|
||||
@@ -187,7 +237,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Progress indicator (collapsed)
|
||||
if (!isExpanded.value)
|
||||
if (!isExpanded)
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -207,14 +257,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
turns: opacityAnimation * 0.5,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Icon(
|
||||
isExpanded.value
|
||||
isExpanded
|
||||
? Symbols.expand_more
|
||||
: Symbols.chevron_right,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
() => isExpanded.value = !isExpanded.value,
|
||||
() => onExpansionChanged?.call(!isExpanded),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
@@ -223,7 +273,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Expanded content
|
||||
if (isExpanded.value)
|
||||
if (isExpanded)
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -243,7 +293,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Clear Completed'),
|
||||
title: const Text('clearCompleted').tr(),
|
||||
leading: Icon(
|
||||
Symbols.clear_all,
|
||||
size: 18,
|
||||
@@ -253,9 +303,35 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(uploadTasksProvider.notifier)
|
||||
.clearCompletedTasks();
|
||||
taskNotifier.clearCompletedTasks();
|
||||
onExpansionChanged?.call(false);
|
||||
},
|
||||
|
||||
tileColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
|
||||
// Clear all tasks button
|
||||
if (activeTasks.any(
|
||||
(task) =>
|
||||
task.status != DriveTaskStatus.completed,
|
||||
))
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: const Text('Clear All'),
|
||||
leading: Icon(
|
||||
Symbols.clear_all,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onTap: () {
|
||||
taskNotifier.clearAllTasks();
|
||||
onExpansionChanged?.call(false);
|
||||
},
|
||||
tileColor:
|
||||
Theme.of(
|
||||
@@ -318,6 +394,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return Symbols.upload;
|
||||
|
||||
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
@@ -339,6 +416,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
if (hasDownload) {
|
||||
return Symbols.download;
|
||||
}
|
||||
return Symbols.upload;
|
||||
} else if (hasPending) {
|
||||
return Symbols.schedule;
|
||||
@@ -356,6 +436,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||
if (tasks.isEmpty) return '0 tasks';
|
||||
|
||||
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||
final hasInProgress = tasks.any(
|
||||
(task) => task.status == DriveTaskStatus.inProgress,
|
||||
);
|
||||
@@ -377,7 +458,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
||||
|
||||
// Priority order: in progress > pending > paused > failed > completed
|
||||
if (hasInProgress) {
|
||||
return '${tasks.length} ${'uploading'.tr()}';
|
||||
if (hasDownload) {
|
||||
return '${tasks.length} ${'downloading'.tr()}';
|
||||
} else {
|
||||
return '${tasks.length} ${'uploading'.tr()}';
|
||||
}
|
||||
} else if (hasPending) {
|
||||
return '${tasks.length} ${'pending'.tr()}';
|
||||
} else if (hasPaused) {
|
||||
@@ -519,7 +604,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
||||
color = Theme.of(context).colorScheme.secondary;
|
||||
break;
|
||||
case DriveTaskStatus.inProgress:
|
||||
icon = Symbols.upload;
|
||||
icon =
|
||||
widget.task.type == 'FileDownload'
|
||||
? Symbols.download
|
||||
: Symbols.upload;
|
||||
color = Theme.of(context).colorScheme.primary;
|
||||
break;
|
||||
case DriveTaskStatus.paused:
|
||||
|
||||
@@ -458,7 +458,7 @@ class FundClaimDialog extends HookConsumerWidget {
|
||||
|
||||
// Remaining amount
|
||||
Text(
|
||||
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / ${remainingSplits} splits',
|
||||
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / $remainingSplits splits',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <syncfusion_pdfviewer_linux/syncfusion_pdfviewer_linux_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <volume_controller/volume_controller_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -90,9 +89,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
|
||||
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
|
||||
@@ -23,7 +23,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
syncfusion_pdfviewer_linux
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
volume_controller
|
||||
window_manager
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import super_native_extensions
|
||||
import syncfusion_pdfviewer_macos
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import volume_controller
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
@@ -86,7 +85,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ PODS:
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- FlutterMacOS
|
||||
- firebase_crashlytics (5.0.4):
|
||||
- firebase_crashlytics (5.0.5):
|
||||
- Firebase/CoreOnly (~> 12.4.0)
|
||||
- Firebase/Crashlytics (~> 12.4.0)
|
||||
- firebase_core
|
||||
@@ -110,7 +110,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- flutter_udid (0.0.1):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain
|
||||
- KeychainAccess
|
||||
- flutter_webrtc (1.2.0):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 137.7151.04)
|
||||
@@ -172,6 +172,7 @@ PODS:
|
||||
- GoogleUtilities/Privacy
|
||||
- irondash_engine_context (0.0.1):
|
||||
- FlutterMacOS
|
||||
- KeychainAccess (4.2.2)
|
||||
- livekit_client (2.5.3):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
@@ -203,7 +204,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- record_macos (1.1.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- share_plus (0.0.1):
|
||||
@@ -249,8 +249,6 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- volume_controller (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (137.7151.04)
|
||||
@@ -298,7 +296,6 @@ DEPENDENCIES:
|
||||
- syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
@@ -317,11 +314,11 @@ SPEC REPOS:
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- KeychainAccess
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
- WebRTC-SDK
|
||||
|
||||
@@ -406,8 +403,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
volume_controller:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
|
||||
wakelock_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||
window_manager:
|
||||
@@ -424,7 +419,7 @@ SPEC CHECKSUMS:
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 09241c4796c1c42a02349ef8bf30025f5b640f0e
|
||||
firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc
|
||||
firebase_crashlytics: c2438b5f5bdcacf59d0eaee5852c6b0ab09dab77
|
||||
firebase_crashlytics: add6934cf4413d6c1307ca5251dbdbfe580e4caf
|
||||
firebase_messaging: 373ac3a56e5aa37bb9aff4127f700aa5973c1168
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
@@ -440,7 +435,7 @@ SPEC CHECKSUMS:
|
||||
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
|
||||
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
|
||||
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
flutter_udid: 00c09e022fd527fd39fef97670b220f2ae8190e7
|
||||
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
@@ -448,6 +443,7 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
livekit_client: a6d5ae8aaeebf3e52235da866fea00f43156c72b
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
|
||||
@@ -461,7 +457,6 @@ SPEC CHECKSUMS:
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
@@ -473,7 +468,6 @@ SPEC CHECKSUMS:
|
||||
syncfusion_pdfviewer_macos: b3b110c68039178ca4105dd03ef38761eca3b36b
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
|
||||
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
|
||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
148
pubspec.lock
148
pubspec.lock
@@ -301,10 +301,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "942a4791cd385a68ccb3b32c71c427aba508a1bb949b86dff2adbe4049f16239"
|
||||
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.3.5+1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -581,18 +581,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
sha256: "80a877f5ec570c4fb3b40720a70b6f31e8bb1315a464b4d3e92fe82754d4b21a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
version: "0.9.3+3"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "88707a3bec4b988aaed3b4df5d7441ee4e987f20b286cddca5d6a8270cab23f2"
|
||||
sha256: "44f24d102e368370951b98ffe86c7325b38349e634578312976607d28cc6d747"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+5"
|
||||
version: "0.9.4+6"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -605,10 +605,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
version: "0.9.3+5"
|
||||
firebase_analytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -661,10 +661,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
||||
sha256: bd4eadd620071ae48ef0e3617499be2a0ea4062d6d5c6fea6c1cec41ebbb2435
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.4"
|
||||
version: "5.0.5"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1132,10 +1132,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
||||
sha256: "31193dbfef74f697e9e5317e59f3c2ae6dc45ce4b9f5d39308a32446e8303acc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.0"
|
||||
flutter_web_plugins:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -1329,26 +1329,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: ca2a3b04d34e76157e9ae680ef16014fb4c2d20484e78417eaed6139330056f6
|
||||
sha256: "788167bcb578b13613b17c3b4a4efae911715f353e36bddc48a0b02a54a8de40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+7"
|
||||
version: "0.8.13+9"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
||||
sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
version: "0.8.13+2"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1497,18 +1497,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: d836715ed95b16b2de3a8c47a88ba5e607976bb1e27c9446d193152ea1429fae
|
||||
sha256: a6a818a35ac5cae780d2e1b21391298dd749959715d6002f9730bd2de80e54a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.2"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "15d9db4ad4d58a11d7269e55d46ff8d49ed5e856226c8a5a91280f0d7c37b3a6"
|
||||
sha256: "668ea65edaab17380956e9713f57e34f78ede505ca0cfd8d39db34e2f260bfee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1521,10 +1521,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_windows
|
||||
sha256: d95535a73eddf57ce5930d5e78a0fa4f294c31981fdeeee83325b797302be454
|
||||
sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
logger:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1649,10 +1649,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a"
|
||||
sha256: eac9b5f27310afe6def49f9b785cef155f99f1db6079d2b2b127b8bddafb6472
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "2.0.0"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1793,18 +1793,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
|
||||
sha256: "95c68a74d3cab950fd0ed8073d9fab15c1c06eb1f3eec68676e87aabc9ecee5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.20"
|
||||
version: "2.2.21"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
||||
sha256: "97390a0719146c7c3e71b6866c34f1cde92685933165c1c671984390d2aca776"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2190,22 +2190,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
screen_brightness_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_platform_interface
|
||||
sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2298,18 +2282,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713"
|
||||
sha256: "07d552dbe8e71ed720e5205e760438ff4ecfb76ec3b32ea664350e2ca4b0c43b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.15"
|
||||
version: "2.4.16"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b"
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2583,18 +2567,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: "8118f13264d1401a7085d12a0aaeac1ebd5cd939046b8c565d195879646daad6"
|
||||
sha256: fea2b5f6c976455d20b19bf77d29bf96a740d14579127b4fc1cdafde42b48177
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_flutter_pdf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_pdf
|
||||
sha256: "34d8b658e9fa7b18c4c16b4a775dc3f634933c4367e5f1ef4854f80cdd22c3ff"
|
||||
sha256: d7852ea65da3e5b64a06b38959cf0de1fb1eaafbfce7ac6950d4a3d59d3cfd57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2607,50 +2591,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_signaturepad
|
||||
sha256: ef891418bee7c79470ff1b6290f7745df8c1b2adf4df6b81ab9cd69ef900c4e8
|
||||
sha256: f74fca0463e0977b7fca6f37fc7dad3cab4b99648594dbe8b99f55f2f5c37ab0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_linux
|
||||
sha256: ec4efb4cdd34462f40b7dafcb5094780a15c988691f28bdec141ea2a01f145bb
|
||||
sha256: "1ffb2e3656694342e29216d7df19961b7fde40d424024efda0610fe5661f1dc3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_macos
|
||||
sha256: "15d4e7d5a5a705b7861bdf7e5758d371973a03fda33a21068dc934569b8fc363"
|
||||
sha256: "166225a4db5c182cd6e18bba69685e15cfe7bd10232c1d5663842636de605437"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_platform_interface
|
||||
sha256: d2b9d4631693503340d5eaef6f42446d6d74f290dca9764e65f5b55b0b4043cf
|
||||
sha256: d630694835967ca78151b22ac88149325e933f1a5de29bf4e845753cc5123585
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_web
|
||||
sha256: "073384338eb9a6370e3ec7b9fbad973b6d0312c027698392c09409e156644807"
|
||||
sha256: "395d3c6f2afb816f9f883b7fe7281a16d3bc38a8b3799e54a1c8399ff91fc059"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
syncfusion_pdfviewer_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_pdfviewer_windows
|
||||
sha256: de6254b5b939c17b32498d895aaf272748035fd20a9790b4ee7e8afe915ef233
|
||||
sha256: ac4a5fdf5eae92163933669d5ec80a3553b84421828d6c7beb167519084a42e4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "31.2.10"
|
||||
version: "31.2.12"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2784,10 +2768,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.3.1"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2816,34 +2800,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9"
|
||||
sha256: "1a109ee074e2a3b17ec3f2785248cb3e93c1e4abab878f637bc33267089f05a3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.24"
|
||||
version: "6.3.26"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9"
|
||||
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.5"
|
||||
version: "6.3.6"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9"
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.4"
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2864,10 +2848,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2932,14 +2916,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
volume_controller:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: volume_controller
|
||||
sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
16
pubspec.yaml
16
pubspec.yaml
@@ -57,30 +57,30 @@ dependencies:
|
||||
cached_network_image: ^3.4.1
|
||||
web: ^1.1.1
|
||||
flutter_blurhash: ^0.9.1
|
||||
media_kit: ^1.2.1
|
||||
media_kit_video: ^1.3.1
|
||||
media_kit: ^1.2.2
|
||||
media_kit_video: ^2.0.0
|
||||
media_kit_libs_video: ^1.0.7
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_platform_alert: ^0.8.0
|
||||
email_validator: ^3.0.0
|
||||
easy_localization: ^3.0.8
|
||||
flutter_inappwebview: ^6.1.5
|
||||
animations: ^2.1.0
|
||||
animations: ^2.1.1
|
||||
package_info_plus: ^9.0.0
|
||||
device_info_plus: ^11.3.0
|
||||
protocol_handler: ^0.2.0
|
||||
tus_client_dart:
|
||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||
cross_file: ^0.3.5
|
||||
cross_file: ^0.3.5+1
|
||||
image_picker: ^1.2.1
|
||||
file_picker: ^10.3.6
|
||||
riverpod_annotation: ^2.6.1
|
||||
image_picker_platform_interface: ^2.11.1
|
||||
image_picker_android: ^0.8.13+7
|
||||
image_picker_android: ^0.8.13+9
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.4
|
||||
flutter_udid: ^4.0.0
|
||||
flutter_udid: ^4.1.0
|
||||
firebase_core: ^4.2.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2874.0
|
||||
@@ -136,7 +136,7 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.4
|
||||
firebase_crashlytics: ^5.0.5
|
||||
firebase_analytics: ^12.0.4
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
@@ -168,7 +168,7 @@ dependencies:
|
||||
event_bus: ^2.0.1
|
||||
convert: ^3.1.2
|
||||
desktop_drop: ^0.7.0
|
||||
flutter_animate: ^4.5.0
|
||||
flutter_animate: ^4.5.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
23
test/drift/app_database/generated/schema.dart
Normal file
23
test/drift/app_database/generated/schema.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// dart format width=80
|
||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v6.dart' as v6;
|
||||
import 'schema_v7.dart' as v7;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 6:
|
||||
return v6.DatabaseAtV6(db);
|
||||
case 7:
|
||||
return v7.DatabaseAtV7(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [6, 7];
|
||||
}
|
||||
1316
test/drift/app_database/generated/schema_v6.dart
Normal file
1316
test/drift/app_database/generated/schema_v6.dart
Normal file
File diff suppressed because it is too large
Load Diff
2672
test/drift/app_database/generated/schema_v7.dart
Normal file
2672
test/drift/app_database/generated/schema_v7.dart
Normal file
File diff suppressed because it is too large
Load Diff
79
test/drift/app_database/migration_test.dart
Normal file
79
test/drift/app_database/migration_test.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: unused_local_variable, unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:island/database/drift_db.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'generated/schema.dart';
|
||||
|
||||
import 'generated/schema_v6.dart' as v6;
|
||||
import 'generated/schema_v7.dart' as v7;
|
||||
|
||||
void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
group('simple database migrations', () {
|
||||
// These simple tests verify all possible schema updates with a simple (no
|
||||
// data) migration. This is a quick way to ensure that written database
|
||||
// migrations properly alter the schema.
|
||||
const versions = GeneratedHelper.versions;
|
||||
for (final (i, fromVersion) in versions.indexed) {
|
||||
group('from $fromVersion', () {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = AppDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// The following template shows how to write tests ensuring your migrations
|
||||
// preserve existing data.
|
||||
// Testing this can be useful for migrations that change existing columns
|
||||
// (e.g. by alterating their type or constraints). Migrations that only add
|
||||
// tables or columns typically don't need these advanced tests. For more
|
||||
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
|
||||
// TODO: This generated template shows how these tests could be written. Adopt
|
||||
// it to your own needs when testing migrations with data integrity.
|
||||
test('migration from v6 to v7 does not corrupt data', () async {
|
||||
// Add data to insert into the old database, and the expected rows after the
|
||||
// migration.
|
||||
// TODO: Fill these lists
|
||||
final oldChatMessagesData = <v6.ChatMessagesData>[];
|
||||
final expectedNewChatMessagesData = <v7.ChatMessagesData>[];
|
||||
|
||||
final oldPostDraftsData = <v6.PostDraftsData>[];
|
||||
final expectedNewPostDraftsData = <v7.PostDraftsData>[];
|
||||
|
||||
await verifier.testWithDataIntegrity(
|
||||
oldVersion: 6,
|
||||
newVersion: 7,
|
||||
createOld: v6.DatabaseAtV6.new,
|
||||
createNew: v7.DatabaseAtV7.new,
|
||||
openTestedDatabase: AppDatabase.new,
|
||||
createItems: (batch, oldDb) {
|
||||
batch.insertAll(oldDb.chatMessages, oldChatMessagesData);
|
||||
batch.insertAll(oldDb.postDrafts, oldPostDraftsData);
|
||||
},
|
||||
validateItems: (newDb) async {
|
||||
expect(
|
||||
expectedNewChatMessagesData,
|
||||
await newDb.select(newDb.chatMessages).get(),
|
||||
);
|
||||
expect(
|
||||
expectedNewPostDraftsData,
|
||||
await newDb.select(newDb.postDrafts).get(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -34,7 +34,6 @@
|
||||
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <volume_controller/volume_controller_plugin_c_api.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
@@ -95,8 +94,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -31,7 +31,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
syncfusion_pdfviewer_windows
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
volume_controller
|
||||
window_manager
|
||||
windows_notification
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user