Compare commits
25 Commits
3.3.0+146
...
56b27c3e82
| Author | SHA1 | Date | |
|---|---|---|---|
|
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.",
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
"amountOfSplits": "Amount of Splits",
|
"amountOfSplits": "Amount of Splits",
|
||||||
"enterNumberOfSplits": "Enter Splits Amount",
|
"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
@@ -2,17 +2,19 @@ import 'dart:convert';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:island/database/message.dart';
|
import 'package:island/database/message.dart';
|
||||||
import 'package:island/database/draft.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';
|
import 'package:island/models/post.dart';
|
||||||
|
|
||||||
part 'drift_db.g.dart';
|
part 'drift_db.g.dart';
|
||||||
|
|
||||||
// Define the database
|
// Define the database
|
||||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase(super.e);
|
AppDatabase(super.e);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 7;
|
int get schemaVersion => 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,7 +185,9 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
await (selectStatement
|
await (selectStatement
|
||||||
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
|
||||||
.get();
|
.get();
|
||||||
return messages.map((msg) => companionToMessage(msg)).toList();
|
final messageFutures =
|
||||||
|
messages.map((msg) => companionToMessage(msg)).toList();
|
||||||
|
return await Future.wait(messageFutures);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert between Drift and model objects
|
// Convert between Drift and model objects
|
||||||
@@ -206,12 +215,45 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalChatMessage companionToMessage(ChatMessage dbMessage) {
|
Future<LocalChatMessage> companionToMessage(ChatMessage dbMessage) async {
|
||||||
final data = jsonDecode(dbMessage.data);
|
final data = jsonDecode(dbMessage.data);
|
||||||
|
SnChatMember? sender;
|
||||||
|
try {
|
||||||
|
final senderRow =
|
||||||
|
await (select(chatMembers)
|
||||||
|
..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
|
||||||
|
final senderAccount = SnAccount.fromJson(senderRow.account);
|
||||||
|
SnAccountStatus? senderStatus;
|
||||||
|
if (senderRow.status != null) {
|
||||||
|
senderStatus = SnAccountStatus.fromJson(jsonDecode(senderRow.status!));
|
||||||
|
}
|
||||||
|
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: senderStatus,
|
||||||
|
lastTyped: senderRow.lastTyped,
|
||||||
|
createdAt: senderRow.createdAt,
|
||||||
|
updatedAt: senderRow.updatedAt,
|
||||||
|
deletedAt: senderRow.deletedAt,
|
||||||
|
chatRoom: null,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
sender = null;
|
||||||
|
}
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: dbMessage.id,
|
id: dbMessage.id,
|
||||||
roomId: dbMessage.roomId,
|
roomId: dbMessage.roomId,
|
||||||
senderId: dbMessage.senderId,
|
senderId: dbMessage.senderId,
|
||||||
|
sender: sender,
|
||||||
data: data,
|
data: data,
|
||||||
createdAt: dbMessage.createdAt,
|
createdAt: dbMessage.createdAt,
|
||||||
status: dbMessage.status,
|
status: dbMessage.status,
|
||||||
@@ -231,6 +273,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
|
// Methods for post drafts
|
||||||
Future<List<SnPost>> getAllPostDrafts() async {
|
Future<List<SnPost>> getAllPostDrafts() async {
|
||||||
final drafts = await select(postDrafts).get();
|
final drafts = await select(postDrafts).get();
|
||||||
|
|||||||
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);
|
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 {
|
class ChatMessages extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
TextColumn get roomId => text()();
|
TextColumn get roomId => text().references(ChatRooms, #id)();
|
||||||
TextColumn get senderId => text()();
|
TextColumn get senderId => text().references(ChatMembers, #id)();
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
TextColumn get nonce => text().nullable()();
|
TextColumn get nonce => text().nullable()();
|
||||||
TextColumn get data => text()();
|
TextColumn get data => text()();
|
||||||
@@ -72,6 +114,7 @@ class LocalChatMessage {
|
|||||||
final String id;
|
final String id;
|
||||||
final String roomId;
|
final String roomId;
|
||||||
final String senderId;
|
final String senderId;
|
||||||
|
final SnChatMember? sender;
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
MessageStatus status;
|
MessageStatus status;
|
||||||
@@ -94,6 +137,7 @@ class LocalChatMessage {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.roomId,
|
required this.roomId,
|
||||||
required this.senderId,
|
required this.senderId,
|
||||||
|
required this.sender,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.nonce,
|
required this.nonce,
|
||||||
@@ -114,7 +158,11 @@ class LocalChatMessage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
SnChatMessage toRemoteMessage() {
|
SnChatMessage toRemoteMessage() {
|
||||||
return SnChatMessage.fromJson(data);
|
final msgData = Map<String, dynamic>.from(data);
|
||||||
|
if (sender != null) {
|
||||||
|
msgData['sender'] = sender!.toJson();
|
||||||
|
}
|
||||||
|
return SnChatMessage.fromJson(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocalChatMessage fromRemoteMessage(
|
static LocalChatMessage fromRemoteMessage(
|
||||||
@@ -122,11 +170,14 @@ class LocalChatMessage {
|
|||||||
MessageStatus status, {
|
MessageStatus status, {
|
||||||
String? nonce,
|
String? nonce,
|
||||||
}) {
|
}) {
|
||||||
|
final msgData = Map<String, dynamic>.from(message.toJson())
|
||||||
|
..remove('sender');
|
||||||
return LocalChatMessage(
|
return LocalChatMessage(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
roomId: message.chatRoomId,
|
roomId: message.chatRoomId,
|
||||||
senderId: message.senderId,
|
senderId: message.senderId,
|
||||||
data: message.toJson(),
|
sender: message.sender,
|
||||||
|
data: msgData,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
status: status,
|
status: status,
|
||||||
nonce: nonce ?? message.nonce,
|
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(),
|
||||||
|
};
|
||||||
@@ -140,8 +140,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
dbMessages =
|
dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map((msg) => _database.companionToMessage(msg))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||||
@@ -202,8 +205,11 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
limit: take,
|
limit: take,
|
||||||
);
|
);
|
||||||
final dbMessages =
|
final dbMessages = await Future.wait(
|
||||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
chatMessagesFromDb
|
||||||
|
.map((msg) => _database.companionToMessage(msg))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
// Always ensure unique messages to prevent duplicate keys
|
// Always ensure unique messages to prevent duplicate keys
|
||||||
final uniqueMessages = <LocalChatMessage>[];
|
final uniqueMessages = <LocalChatMessage>[];
|
||||||
@@ -300,7 +306,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
|||||||
final lastMessage =
|
final lastMessage =
|
||||||
dbMessages.isEmpty
|
dbMessages.isEmpty
|
||||||
? null
|
? null
|
||||||
: _database.companionToMessage(dbMessages.first);
|
: await _database.companionToMessage(dbMessages.first);
|
||||||
|
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
talker.log('No local messages, fetching from network');
|
talker.log('No local messages, fetching from network');
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ part 'file_list.g.dart';
|
|||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
|
String? _poolId;
|
||||||
|
String? _query;
|
||||||
|
String? _order;
|
||||||
|
bool _orderDesc = false;
|
||||||
|
|
||||||
void setPath(String path) {
|
void setPath(String path) {
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
ref.invalidateSelf();
|
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
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -26,9 +50,25 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
}) async {
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
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(
|
final response = await client.get(
|
||||||
'/drive/index/browse',
|
'/drive/index/browse',
|
||||||
queryParameters: {'path': _currentPath},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<String> folders =
|
final List<String> folders =
|
||||||
@@ -58,6 +98,37 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
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
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -70,9 +141,32 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||||
const take = 50; // Default page size
|
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(
|
final response = await client.get(
|
||||||
'/drive/index/unindexed',
|
'/drive/index/unindexed',
|
||||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||||
String _$cloudFileListNotifierHash() =>
|
String _$cloudFileListNotifierHash() =>
|
||||||
r'5f2f80357cb31ac6473df5ac2101f9a462004f81';
|
r'533dfa86f920b60cf7491fb4aeb95ece19e428af';
|
||||||
|
|
||||||
/// See also [CloudFileListNotifier].
|
/// See also [CloudFileListNotifier].
|
||||||
@ProviderFor(CloudFileListNotifier)
|
@ProviderFor(CloudFileListNotifier)
|
||||||
@@ -66,7 +66,7 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
typedef _$CloudFileListNotifier =
|
typedef _$CloudFileListNotifier =
|
||||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
String _$unindexedFileListNotifierHash() =>
|
String _$unindexedFileListNotifierHash() =>
|
||||||
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
r'afa487d7b956b71b21ca1b073a01364a34ede1d5';
|
||||||
|
|
||||||
/// See also [UnindexedFileListNotifier].
|
/// See also [UnindexedFileListNotifier].
|
||||||
@ProviderFor(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();
|
}).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) {
|
void removeTask(String taskId) {
|
||||||
state = state.where((task) => task.taskId != taskId).toList();
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
}
|
}
|
||||||
@@ -275,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearAllTasks() {
|
||||||
|
state = [];
|
||||||
|
}
|
||||||
|
|
||||||
DriveTask? getTask(String taskId) {
|
DriveTask? getTask(String taskId) {
|
||||||
return state.where((task) => task.taskId == taskId).firstOrNull;
|
return state.where((task) => task.taskId == taskId).firstOrNull;
|
||||||
}
|
}
|
||||||
@@ -291,6 +313,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_websocketSubscription?.cancel();
|
_websocketSubscription?.cancel();
|
||||||
|
|||||||
@@ -170,6 +170,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
builder: (context, state) => const AboutScreen(),
|
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
|
// Main tabs with TabsScreen shell
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _tabsShellKey,
|
navigatorKey: _tabsShellKey,
|
||||||
@@ -427,23 +443,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
path: '/files',
|
path: '/files',
|
||||||
builder: (context, state) => const FileListScreen(),
|
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
|
// SN-chan tab
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class CaptchaScreen extends ConsumerWidget {
|
|||||||
return showModalBottomSheet<String>(
|
return showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
isDismissible: false,
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class CaptchaScreen extends ConsumerStatefulWidget {
|
|||||||
static Future<String?> show(BuildContext context) {
|
static Future<String?> show(BuildContext context) {
|
||||||
return showModalBottomSheet<String>(
|
return showModalBottomSheet<String>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isDismissible: false,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => const CaptchaScreen(),
|
builder: (context) => const CaptchaScreen(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -6,6 +8,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/chat.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/call.dart';
|
||||||
import 'package:island/pods/chat/chat_summary.dart';
|
import 'package:island/pods/chat/chat_summary.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@@ -121,6 +126,17 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String titleText;
|
||||||
|
if (isDirect && room.name == null) {
|
||||||
|
if (room.members?.isNotEmpty ?? false) {
|
||||||
|
titleText = room.members!.map((e) => e.account.nick).join(', ');
|
||||||
|
} else {
|
||||||
|
titleText = 'Direct Message';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
titleText = room.name ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Badge(
|
leading: Badge(
|
||||||
isLabelVisible: summary.when(
|
isLabelVisible: summary.when(
|
||||||
@@ -140,11 +156,7 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(titleText),
|
||||||
(isDirect && room.name == null)
|
|
||||||
? room.members!.map((e) => e.account.nick).join(', ')
|
|
||||||
: room.name ?? '',
|
|
||||||
),
|
|
||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
trailing: trailing, // Add this line
|
trailing: trailing, // Add this line
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -162,12 +174,92 @@ class ChatRoomListTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
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 client = ref.watch(apiClientProvider);
|
||||||
final resp = await client.get('/sphere/chat');
|
final resp = await client.get('/sphere/chat');
|
||||||
return resp.data
|
final rooms =
|
||||||
.map((e) => SnChatRoom.fromJson(e))
|
resp.data.map((e) => SnChatRoom.fromJson(e)).cast<SnChatRoom>().toList();
|
||||||
.cast<SnChatRoom>()
|
await db.saveChatRooms(rooms);
|
||||||
.toList();
|
return rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatListBodyWidget extends HookConsumerWidget {
|
class ChatListBodyWidget extends HookConsumerWidget {
|
||||||
|
|||||||
@@ -6,17 +6,24 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/pods/file_references.dart';
|
||||||
import 'package:island/pods/network.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/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||||
import 'package:island/widgets/content/file_viewer_contents.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/path.dart' show extension;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class FileDetailScreen extends HookConsumerWidget {
|
class FileDetailScreen extends HookConsumerWidget {
|
||||||
final SnCloudFile item;
|
final SnCloudFile item;
|
||||||
@@ -76,7 +83,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}, [animationController]);
|
}, [animationController]);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: true,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -86,26 +93,47 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||||
),
|
),
|
||||||
body: AnimatedBuilder(
|
body: LayoutBuilder(
|
||||||
animation: animation,
|
builder: (context, constraints) {
|
||||||
builder: (context, child) {
|
return AnimatedBuilder(
|
||||||
return Row(
|
animation: animation,
|
||||||
children: [
|
builder: (context, child) {
|
||||||
// Main content area
|
return Stack(
|
||||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
children: [
|
||||||
// Animated drawer panel
|
// Main content area - resizes with animation
|
||||||
if (isWide)
|
Positioned(
|
||||||
SizedBox(
|
left: 0,
|
||||||
height: double.infinity,
|
top: 0,
|
||||||
width: animation.value * 400, // Max width of 400px
|
bottom: 0,
|
||||||
child: Container(
|
width: constraints.maxWidth - animation.value * 400,
|
||||||
child:
|
child: _buildContent(context, ref, serverUrl),
|
||||||
animation.value > 0.1
|
|
||||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
// 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;
|
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
|
// Always add info button
|
||||||
actions.add(
|
actions.add(
|
||||||
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
IconButton(icon: Icon(Icons.info_outline), onPressed: showInfoSheet),
|
||||||
@@ -187,6 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadFile(WidgetRef ref) async {
|
Future<void> _downloadFile(WidgetRef ref) async {
|
||||||
|
final taskNotifier = ref.read(uploadTasksProvider.notifier);
|
||||||
|
final taskId = taskNotifier.addLocalDownloadTask(item);
|
||||||
try {
|
try {
|
||||||
showSnackBar('Downloading file...');
|
showSnackBar('Downloading file...');
|
||||||
|
|
||||||
@@ -202,14 +250,26 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
'/drive/files/${item.id}',
|
'/drive/files/${item.id}',
|
||||||
filePath,
|
filePath,
|
||||||
queryParameters: {'original': true},
|
queryParameters: {'original': true},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
taskNotifier.updateDownloadProgress(taskId, count, total);
|
||||||
|
taskNotifier.updateTransmissionProgress(taskId, count / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
await FileSaver.instance.saveFile(
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
file: File(filePath),
|
file: File(filePath),
|
||||||
);
|
);
|
||||||
|
taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||||
showSnackBar('File saved to downloads');
|
showSnackBar('File saved to downloads');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
taskNotifier.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
DriveTaskStatus.failed,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
);
|
||||||
showErrorAlert(e);
|
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:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_list.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
@@ -23,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
// Path navigation state
|
// Path navigation state
|
||||||
final currentPath = useState<String>('/');
|
final currentPath = useState<String>('/');
|
||||||
final mode = useState<FileListMode>(FileListMode.normal);
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
final selectedPool = useState<SnFilePool?>(null);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
@@ -32,7 +35,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Files'),
|
title: Text('files').tr(),
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -55,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
usage: usage,
|
usage: usage,
|
||||||
quota: quota,
|
quota: quota,
|
||||||
currentPath: currentPath,
|
currentPath: currentPath,
|
||||||
|
selectedPool: selectedPool,
|
||||||
onPickAndUpload:
|
onPickAndUpload:
|
||||||
() => _pickAndUploadFile(ref, currentPath.value),
|
() => _pickAndUploadFile(
|
||||||
|
ref,
|
||||||
|
currentPath.value,
|
||||||
|
selectedPool.value?.id,
|
||||||
|
),
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
viewMode: viewMode,
|
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 {
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@@ -92,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
fileData: universalFile,
|
fileData: universalFile,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
|
poolId: poolId,
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
// Progress is handled by the upload tasks system
|
// Progress is handled by the upload tasks system
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class AccountName extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
account.nick,
|
textOverride ?? account.nick,
|
||||||
style: nameStyle,
|
style: nameStyle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
@@ -171,6 +172,24 @@ class CloudFileLightbox extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: showInfoSheet,
|
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(),
|
Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/utils/format.dart';
|
import 'package:island/utils/format.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.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:styled_widget/styled_widget.dart';
|
||||||
import 'package:island/widgets/data_saving_gate.dart';
|
import 'package:island/widgets/data_saving_gate.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
|
||||||
|
|
||||||
import 'file_viewer_contents.dart';
|
import 'file_viewer_contents.dart';
|
||||||
import 'image.dart';
|
import 'image.dart';
|
||||||
@@ -66,34 +59,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (item.mimeType == 'application/pdf') {
|
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(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -166,30 +131,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -201,34 +156,6 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.mimeType?.startsWith('text/') == true) {
|
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(
|
return Container(
|
||||||
height: 400,
|
height: 400,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -304,30 +231,20 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.download,
|
Symbols.more_horiz,
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
onPressed: downloadFile,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.info,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.all(4),
|
padding: EdgeInsets.all(4),
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -356,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
'audio' => AudioFileContent(item: item, uri: uri),
|
'audio' => AudioFileContent(item: item, uri: uri),
|
||||||
_ => Builder(
|
_ => Builder(
|
||||||
builder: (context) {
|
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(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).colorScheme.outline,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -422,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
|
||||||
onPressed: downloadFile,
|
|
||||||
icon: const Icon(Symbols.download),
|
|
||||||
label: Text('download').tr(),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
context.pushNamed(
|
||||||
useRootNavigator: true,
|
'fileDetail',
|
||||||
context: context,
|
pathParameters: {'id': item.id},
|
||||||
isScrollControlled: true,
|
extra: item,
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.info),
|
icon: const Icon(Symbols.info),
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -28,8 +30,64 @@ class PdfFileContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final pdfViewer = useMemoized(() => SfPdfViewer.network(uri), [uri]);
|
final fileFuture = useMemoized(
|
||||||
return pdfViewer;
|
() => 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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: PhotoView(
|
child: Listener(
|
||||||
backgroundDecoration: BoxDecoration(
|
onPointerSignal: (pointerSignal) {
|
||||||
color: Colors.black.withOpacity(0.9),
|
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
|
// Controls overlay
|
||||||
@@ -245,68 +323,57 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Column(
|
||||||
margin: const EdgeInsets.all(32),
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.all(32),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Icon(
|
||||||
border: Border.all(
|
Symbols.insert_drive_file,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
size: 64,
|
||||||
width: 1,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
const Gap(16),
|
||||||
),
|
Text(
|
||||||
child: Column(
|
item.name,
|
||||||
mainAxisSize: MainAxisSize.min,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 20,
|
||||||
Icon(
|
fontWeight: FontWeight.bold,
|
||||||
Symbols.insert_drive_file,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
size: 64,
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
formatFileSize(item.size),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const Gap(16),
|
),
|
||||||
Text(
|
const Gap(24),
|
||||||
item.name,
|
Row(
|
||||||
style: TextStyle(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 20,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
FilledButton.icon(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
onPressed: downloadFile,
|
||||||
|
icon: const Icon(Symbols.download),
|
||||||
|
label: Text('download').tr(),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
const Gap(16),
|
||||||
),
|
OutlinedButton.icon(
|
||||||
const Gap(8),
|
onPressed: () {
|
||||||
Text(
|
showModalBottomSheet(
|
||||||
formatFileSize(item.size),
|
useRootNavigator: true,
|
||||||
style: TextStyle(
|
context: context,
|
||||||
fontSize: 16,
|
isScrollControlled: true,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
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 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
@@ -28,28 +25,12 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
|||||||
VideoController? _videoController;
|
VideoController? _videoController;
|
||||||
|
|
||||||
void _openVideo() async {
|
void _openVideo() async {
|
||||||
final url = widget.uri;
|
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
_player = Player();
|
_player = Player();
|
||||||
_videoController = VideoController(_player!);
|
_videoController = VideoController(_player!);
|
||||||
|
|
||||||
String? uri;
|
_player!.open(Media(widget.uri), play: widget.autoplay);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Enter access token',
|
hintText: 'Enter access token',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -457,7 +457,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
maxLines: 6,
|
maxLines: 6,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(
|
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)
|
// Return the fund that was just created (but not yet paid)
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
|
Navigator.of(context).pop(fund);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop(fund);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,10 +327,10 @@ class ComposeFundSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(updatedFund);
|
||||||
}
|
}
|
||||||
Navigator.of(
|
|
||||||
context,
|
|
||||||
).pop(updatedFund);
|
|
||||||
} else {
|
} else {
|
||||||
isPushing.value = false;
|
isPushing.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@@ -29,7 +29,49 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
..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(
|
final slideController = useAnimationController(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
@@ -63,6 +105,8 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
position: slideAnimation,
|
position: slideAnimation,
|
||||||
child: _UploadOverlayContent(
|
child: _UploadOverlayContent(
|
||||||
activeTasks: activeTasks,
|
activeTasks: activeTasks,
|
||||||
|
isExpanded: isExpandedLocal.value,
|
||||||
|
onExpansionChanged: (expanded) => isExpandedLocal.value = expanded,
|
||||||
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -71,12 +115,17 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
|
|
||||||
class _UploadOverlayContent extends HookConsumerWidget {
|
class _UploadOverlayContent extends HookConsumerWidget {
|
||||||
final List<DriveTask> activeTasks;
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isExpanded = useState(false);
|
|
||||||
final animationController = useAnimationController(
|
final animationController = useAnimationController(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
initialValue: 0.0,
|
initialValue: 0.0,
|
||||||
@@ -91,15 +140,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (isExpanded.value) {
|
if (isExpanded) {
|
||||||
animationController.forward();
|
animationController.forward();
|
||||||
} else {
|
} else {
|
||||||
animationController.reverse();
|
animationController.reverse();
|
||||||
}
|
}
|
||||||
return null;
|
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(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
@@ -108,7 +159,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
right: isMobile ? 16 : 24,
|
right: isMobile ? 16 : 24,
|
||||||
),
|
),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => isExpanded.value = !isExpanded.value,
|
onTap: () => onExpansionChanged?.call(!isExpanded),
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: animationController,
|
animation: animationController,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
@@ -142,8 +193,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Icon(
|
child: Icon(
|
||||||
key: ValueKey(isExpanded.value),
|
key: ValueKey(isExpanded),
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? Symbols.list_rounded
|
? Symbols.list_rounded
|
||||||
: _getOverallStatusIcon(activeTasks),
|
: _getOverallStatusIcon(activeTasks),
|
||||||
size: 24,
|
size: 24,
|
||||||
@@ -159,7 +210,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? 'uploadTasks'.tr()
|
? 'uploadTasks'.tr()
|
||||||
: _getOverallStatusText(activeTasks),
|
: _getOverallStatusText(activeTasks),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
@@ -169,8 +220,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (!isExpanded.value &&
|
if (!isExpanded && activeTasks.isNotEmpty)
|
||||||
activeTasks.isNotEmpty)
|
|
||||||
Text(
|
Text(
|
||||||
_getOverallProgressText(activeTasks),
|
_getOverallProgressText(activeTasks),
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
@@ -187,7 +237,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Progress indicator (collapsed)
|
// Progress indicator (collapsed)
|
||||||
if (!isExpanded.value)
|
if (!isExpanded)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -207,14 +257,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
turns: opacityAnimation * 0.5,
|
turns: opacityAnimation * 0.5,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isExpanded.value
|
isExpanded
|
||||||
? Symbols.expand_more
|
? Symbols.expand_more
|
||||||
: Symbols.chevron_right,
|
: Symbols.chevron_right,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed:
|
||||||
() => isExpanded.value = !isExpanded.value,
|
() => onExpansionChanged?.call(!isExpanded),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
@@ -223,7 +273,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Expanded content
|
// Expanded content
|
||||||
if (isExpanded.value)
|
if (isExpanded)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -243,7 +293,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('Clear Completed'),
|
title: const Text('clearCompleted').tr(),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.clear_all,
|
Symbols.clear_all,
|
||||||
size: 18,
|
size: 18,
|
||||||
@@ -253,9 +303,35 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
).colorScheme.onSurfaceVariant,
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
taskNotifier.clearCompletedTasks();
|
||||||
.read(uploadTasksProvider.notifier)
|
onExpansionChanged?.call(false);
|
||||||
.clearCompletedTasks();
|
},
|
||||||
|
|
||||||
|
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:
|
tileColor:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@@ -318,6 +394,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return Symbols.upload;
|
if (tasks.isEmpty) return Symbols.upload;
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -339,6 +416,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
|
if (hasDownload) {
|
||||||
|
return Symbols.download;
|
||||||
|
}
|
||||||
return Symbols.upload;
|
return Symbols.upload;
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return Symbols.schedule;
|
return Symbols.schedule;
|
||||||
@@ -356,6 +436,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return '0 tasks';
|
if (tasks.isEmpty) return '0 tasks';
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -377,7 +458,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
return '${tasks.length} ${'uploading'.tr()}';
|
if (hasDownload) {
|
||||||
|
return '${tasks.length} ${'downloading'.tr()}';
|
||||||
|
} else {
|
||||||
|
return '${tasks.length} ${'uploading'.tr()}';
|
||||||
|
}
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return '${tasks.length} ${'pending'.tr()}';
|
return '${tasks.length} ${'pending'.tr()}';
|
||||||
} else if (hasPaused) {
|
} else if (hasPaused) {
|
||||||
@@ -519,7 +604,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
color = Theme.of(context).colorScheme.secondary;
|
color = Theme.of(context).colorScheme.secondary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.inProgress:
|
case DriveTaskStatus.inProgress:
|
||||||
icon = Symbols.upload;
|
icon =
|
||||||
|
widget.task.type == 'FileDownload'
|
||||||
|
? Symbols.download
|
||||||
|
: Symbols.upload;
|
||||||
color = Theme.of(context).colorScheme.primary;
|
color = Theme.of(context).colorScheme.primary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.paused:
|
case DriveTaskStatus.paused:
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ class FundClaimDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Remaining amount
|
// Remaining amount
|
||||||
Text(
|
Text(
|
||||||
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / ${remainingSplits} splits',
|
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / $remainingSplits splits',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user