🔀 Merge pull request '♻️ 使用 SQLITE 来存储本地消息记录' (#1) from features/local-message-history into master

Reviewed-on: #1
This commit is contained in:
LittleSheep 2024-06-23 11:13:41 +00:00
commit 6d755fc1b7
21 changed files with 1042 additions and 285 deletions

View File

@ -21,8 +21,8 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule avoid_print: true # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,79 @@
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/message.dart';
import 'package:solian/providers/message/helper.dart';
import 'package:solian/providers/message/history.dart';
class ChatHistoryController {
late final MessageHistoryDb database;
final RxList<LocalMessage> currentHistory = RxList.empty(growable: true);
final RxInt totalHistoryCount = 0.obs;
final RxBool isLoading = false.obs;
initialize() async {
database = await createHistoryDb();
currentHistory.clear();
}
Future<void> getMessages(Channel channel, String scope) async {
isLoading.value = true;
totalHistoryCount.value =
await database.syncMessages(channel, scope: scope);
await syncHistory(channel);
isLoading.value = false;
}
Future<void> getMoreMessages(Channel channel, String scope) async {
isLoading.value = true;
totalHistoryCount.value = await database.syncMessages(
channel,
breath: 3,
scope: scope,
offset: currentHistory.length,
);
await syncHistory(channel);
isLoading.value = false;
}
Future<void> syncHistory(Channel channel) async {
currentHistory.replaceRange(0, currentHistory.length,
await database.localMessages.findAllByChannel(channel.id));
}
receiveMessage(Message remote) async {
final entry = await database.receiveMessage(remote);
final idx = currentHistory.indexWhere((x) => x.data.uuid == remote.uuid);
if (idx != -1) {
currentHistory[idx] = entry;
} else {
currentHistory.insert(0, entry);
}
}
addTemporaryMessage(Message info) async {
currentHistory.insert(
0,
LocalMessage(
info.id,
info,
info.channelId,
),
);
}
void replaceMessage(Message remote) async {
final entry = await database.replaceMessage(remote);
currentHistory.replaceRange(
0,
currentHistory.length,
currentHistory.map((x) => x.id == entry.id ? entry : x),
);
}
void burnMessage(int id) async {
await database.burnMessage(id);
currentHistory.removeWhere((x) => x.id == id);
}
}

View File

@ -33,6 +33,7 @@ void main() async {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
if (PlatformInfo.isDesktop) { if (PlatformInfo.isDesktop) {
await Window.initialize(); await Window.initialize();
await Window.setEffect( await Window.setEffect(

View File

@ -81,24 +81,24 @@ class AccountBadge {
}); });
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge( factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge(
id: json["id"], id: json['id'],
accountId: json["account_id"], accountId: json['account_id'],
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
deletedAt: json["deleted_at"] != null deletedAt: json['deleted_at'] != null
? DateTime.parse(json["deleted_at"]) ? DateTime.parse(json['deleted_at'])
: null, : null,
metadata: json["metadata"], metadata: json['metadata'],
type: json["type"], type: json['type'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"account_id": accountId, 'account_id': accountId,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
"metadata": metadata, 'metadata': metadata,
"type": type, 'type': type,
}; };
} }

View File

@ -38,40 +38,40 @@ class Attachment {
}); });
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
uuid: json["uuid"], uuid: json['uuid'],
size: json["size"], size: json['size'],
name: json["name"], name: json['name'],
alt: json["alt"], alt: json['alt'],
usage: json["usage"], usage: json['usage'],
mimetype: json["mimetype"], mimetype: json['mimetype'],
hash: json["hash"], hash: json['hash'],
destination: json["destination"], destination: json['destination'],
metadata: json["metadata"], metadata: json['metadata'],
isMature: json["is_mature"], isMature: json['is_mature'],
account: Account.fromJson(json["account"]), account: Account.fromJson(json['account']),
accountId: json["account_id"], accountId: json['account_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"uuid": uuid, 'uuid': uuid,
"size": size, 'size': size,
"name": name, 'name': name,
"alt": alt, 'alt': alt,
"usage": usage, 'usage': usage,
"mimetype": mimetype, 'mimetype': mimetype,
"hash": hash, 'hash': hash,
"destination": destination, 'destination': destination,
"metadata": metadata, 'metadata': metadata,
"is_mature": isMature, 'is_mature': isMature,
"account": account.toJson(), 'account': account.toJson(),
"account_id": accountId, 'account_id': accountId,
}; };
} }

View File

@ -44,10 +44,11 @@ class Message {
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'],
content: json['content'], content: json['content'],
type: json['type'], type: json['type'],
attachments: json["attachments"] != null attachments: json['attachments'] != null
? List<int>.from(json["attachments"]) ? List<int>.from(json['attachments'])
: null, : null,
channel: Channel.fromJson(json['channel']), channel:
json['channel'] != null ? Channel.fromJson(json['channel']) : null,
sender: Sender.fromJson(json['sender']), sender: Sender.fromJson(json['sender']),
replyId: json['reply_id'], replyId: json['reply_id'],
replyTo: json['reply_to'] != null replyTo: json['reply_to'] != null

View File

@ -53,36 +53,36 @@ class Post {
}); });
factory Post.fromJson(Map<String, dynamic> json) => Post( factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"] != null deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at']) ? DateTime.parse(json['deleted_at'])
: null, : null,
alias: json["alias"], alias: json['alias'],
content: json["content"], content: json['content'],
tags: json["tags"], tags: json['tags'],
categories: json["categories"], categories: json['categories'],
reactions: json["reactions"], reactions: json['reactions'],
replies: json["replies"], replies: json['replies'],
attachments: json["attachments"] != null attachments: json['attachments'] != null
? List<int>.from(json["attachments"]) ? List<int>.from(json['attachments'])
: null, : null,
replyId: json["reply_id"], replyId: json['reply_id'],
repostId: json["repost_id"], repostId: json['repost_id'],
realmId: json["realm_id"], realmId: json['realm_id'],
replyTo: replyTo:
json["reply_to"] != null ? Post.fromJson(json["reply_to"]) : null, json['reply_to'] != null ? Post.fromJson(json['reply_to']) : null,
repostTo: repostTo:
json["repost_to"] != null ? Post.fromJson(json["repost_to"]) : null, json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null,
realm: json["realm"] != null ? Realm.fromJson(json["realm"]) : null, realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
publishedAt: json["published_at"] != null ? DateTime.parse(json["published_at"]) : null, publishedAt: json['published_at'] != null ? DateTime.parse(json['published_at']) : null,
authorId: json["author_id"], authorId: json['author_id'],
author: Account.fromJson(json["author"]), author: Account.fromJson(json['author']),
replyCount: json["reply_count"], replyCount: json['reply_count'],
reactionCount: json["reaction_count"], reactionCount: json['reaction_count'],
reactionList: json["reaction_list"] != null reactionList: json['reaction_list'] != null
? json["reaction_list"] ? json['reaction_list']
.map((key, value) => MapEntry( .map((key, value) => MapEntry(
key, key,
int.tryParse(value.toString()) ?? int.tryParse(value.toString()) ??
@ -92,28 +92,28 @@ class Post {
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"alias": alias, 'alias': alias,
"content": content, 'content': content,
"tags": tags, 'tags': tags,
"categories": categories, 'categories': categories,
"reactions": reactions, 'reactions': reactions,
"replies": replies, 'replies': replies,
"attachments": attachments, 'attachments': attachments,
"reply_id": replyId, 'reply_id': replyId,
"repost_id": repostId, 'repost_id': repostId,
"realm_id": realmId, 'realm_id': realmId,
"reply_to": replyTo?.toJson(), 'reply_to': replyTo?.toJson(),
"repost_to": repostTo?.toJson(), 'repost_to': repostTo?.toJson(),
"realm": realm?.toJson(), 'realm': realm?.toJson(),
"published_at": publishedAt?.toIso8601String(), 'published_at': publishedAt?.toIso8601String(),
"author_id": authorId, 'author_id': authorId,
"author": author.toJson(), 'author': author.toJson(),
"reply_count": replyCount, 'reply_count': replyCount,
"reaction_count": reactionCount, 'reaction_count': reactionCount,
"reaction_list": reactionList, 'reaction_list': reactionList,
}; };
} }

View File

@ -190,10 +190,10 @@ class AccountProvider extends GetxController {
} }
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
provider = "apple"; provider = 'apple';
token = await FirebaseMessaging.instance.getAPNSToken(); token = await FirebaseMessaging.instance.getAPNSToken();
} else { } else {
provider = "firebase"; provider = 'firebase';
token = await FirebaseMessaging.instance.getToken(); token = await FirebaseMessaging.instance.getToken();
} }

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart'; import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_history_controller.dart';
import 'package:solian/providers/account.dart'; import 'package:solian/providers/account.dart';
import 'package:solian/providers/chat.dart'; import 'package:solian/providers/chat.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -79,7 +80,7 @@ class AuthProvider extends GetConnect {
if (credentials!.isExpired) { if (credentials!.isExpired) {
await refreshCredentials(); await refreshCredentials();
log("Refreshed credentials at ${DateTime.now()}"); log('Refreshed credentials at ${DateTime.now()}');
} }
} }
@ -135,6 +136,11 @@ class AuthProvider extends GetConnect {
Get.find<AccountProvider>().notifications.clear(); Get.find<AccountProvider>().notifications.clear();
Get.find<AccountProvider>().notificationUnread.value = 0; Get.find<AccountProvider>().notificationUnread.value = 0;
final chatHistory = ChatHistoryController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localMessages.wipeLocalMessages();
});
storage.deleteAll(); storage.deleteAll();
} }

View File

@ -0,0 +1,108 @@
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/message.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/message/history.dart';
Future<MessageHistoryDb> createHistoryDb() async {
return await $FloorMessageHistoryDb
.databaseBuilder('messaging_data.dart')
.build();
}
extension MessageHistoryHelper on MessageHistoryDb {
receiveMessage(Message remote) async {
final entry = LocalMessage(
remote.id,
remote,
remote.channelId,
);
await localMessages.insert(entry);
return entry;
}
replaceMessage(Message remote) async {
final entry = LocalMessage(
remote.id,
remote,
remote.channelId,
);
await localMessages.update(entry);
return entry;
}
burnMessage(int id) async {
await localMessages.delete(id);
}
syncMessages(Channel channel, {String scope = 'global', breath = 10, offset = 0}) async {
final lastOne = await localMessages.findLastByChannel(channel.id);
final data = await _getRemoteMessages(
channel,
scope,
remainBreath: breath,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await localMessages.insertBulk(
data.$1.map((x) => LocalMessage(x.id, x, x.channelId)).toList(),
);
}
return data?.$2 ?? 0;
}
Future<(List<Message>, int)?> _getRemoteMessages(
Channel channel,
String scope, {
required int remainBreath,
bool Function(List<Message> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainBreath <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/api/channels/$scope/${channel.alias}/messages?take=$take&offset=$offset');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Message.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await _getRemoteMessages(
channel,
scope,
remainBreath: remainBreath - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
Future<List<LocalMessage>> listMessages(Channel channel) async {
return await localMessages.findAllByChannel(channel.id);
}
}

View File

@ -0,0 +1,66 @@
import 'dart:async';
import 'dart:convert';
import 'package:floor/floor.dart';
import 'package:solian/models/message.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
part 'history.g.dart';
@entity
class LocalMessage {
@primaryKey
final int id;
final Message data;
final int channelId;
LocalMessage(this.id, this.data, this.channelId);
}
class RemoteMessageConverter extends TypeConverter<Message, String> {
@override
Message decode(String databaseValue) {
return Message.fromJson(jsonDecode(databaseValue));
}
@override
String encode(Message value) {
return jsonEncode(value.toJson());
}
}
@dao
abstract class LocalMessageDao {
@Query('SELECT COUNT(id) FROM LocalMessage WHERE channelId = :channelId')
Future<int?> countByChannel(int channelId);
@Query('SELECT * FROM LocalMessage WHERE channelId = :channelId ORDER BY id DESC')
Future<List<LocalMessage>> findAllByChannel(int channelId);
@Query('SELECT * FROM LocalMessage WHERE channelId = :channelId ORDER BY id DESC LIMIT 1')
Future<LocalMessage?> findLastByChannel(int channelId);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insert(LocalMessage m);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertBulk(List<LocalMessage> m);
@Update(onConflict: OnConflictStrategy.replace)
Future<void> update(LocalMessage person);
@Query('DELETE FROM LocalMessage WHERE id = :id')
Future<void> delete(int id);
@Query('DELETE FROM LocalMessage WHERE channelId = :channelId')
Future<List<LocalMessage>> deleteByChannel(int channelId);
@Query('DELETE FROM LocalMessage')
Future<void> wipeLocalMessages();
}
@TypeConverters([RemoteMessageConverter])
@Database(version: 1, entities: [LocalMessage])
abstract class MessageHistoryDb extends FloorDatabase {
LocalMessageDao get localMessages;
}

View File

@ -0,0 +1,214 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'history.dart';
// **************************************************************************
// FloorGenerator
// **************************************************************************
abstract class $MessageHistoryDbBuilderContract {
/// Adds migrations to the builder.
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations);
/// Adds a database [Callback] to the builder.
$MessageHistoryDbBuilderContract addCallback(Callback callback);
/// Creates the database and initializes it.
Future<MessageHistoryDb> build();
}
// ignore: avoid_classes_with_only_static_members
class $FloorMessageHistoryDb {
/// Creates a database builder for a persistent database.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract databaseBuilder(String name) =>
_$MessageHistoryDbBuilder(name);
/// Creates a database builder for an in memory database.
/// Information stored in an in memory database disappears when the process is killed.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract inMemoryDatabaseBuilder() =>
_$MessageHistoryDbBuilder(null);
}
class _$MessageHistoryDbBuilder implements $MessageHistoryDbBuilderContract {
_$MessageHistoryDbBuilder(this.name);
final String? name;
final List<Migration> _migrations = [];
Callback? _callback;
@override
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations) {
_migrations.addAll(migrations);
return this;
}
@override
$MessageHistoryDbBuilderContract addCallback(Callback callback) {
_callback = callback;
return this;
}
@override
Future<MessageHistoryDb> build() async {
final path = name != null
? await sqfliteDatabaseFactory.getDatabasePath(name!)
: ':memory:';
final database = _$MessageHistoryDb();
database.database = await database.open(
path,
_migrations,
_callback,
);
return database;
}
}
class _$MessageHistoryDb extends MessageHistoryDb {
_$MessageHistoryDb([StreamController<String>? listener]) {
changeListener = listener ?? StreamController<String>.broadcast();
}
LocalMessageDao? _localMessagesInstance;
Future<sqflite.Database> open(
String path,
List<Migration> migrations, [
Callback? callback,
]) async {
final databaseOptions = sqflite.OpenDatabaseOptions(
version: 1,
onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
await callback?.onConfigure?.call(database);
},
onOpen: (database) async {
await callback?.onOpen?.call(database);
},
onUpgrade: (database, startVersion, endVersion) async {
await MigrationAdapter.runMigrations(
database, startVersion, endVersion, migrations);
await callback?.onUpgrade?.call(database, startVersion, endVersion);
},
onCreate: (database, version) async {
await database.execute(
'CREATE TABLE IF NOT EXISTS `LocalMessage` (`id` INTEGER NOT NULL, `data` TEXT NOT NULL, `channelId` INTEGER NOT NULL, PRIMARY KEY (`id`))');
await callback?.onCreate?.call(database, version);
},
);
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
}
@override
LocalMessageDao get localMessages {
return _localMessagesInstance ??=
_$LocalMessageDao(database, changeListener);
}
}
class _$LocalMessageDao extends LocalMessageDao {
_$LocalMessageDao(
this.database,
this.changeListener,
) : _queryAdapter = QueryAdapter(database),
_localMessageInsertionAdapter = InsertionAdapter(
database,
'LocalMessage',
(LocalMessage item) => <String, Object?>{
'id': item.id,
'data': _remoteMessageConverter.encode(item.data),
'channelId': item.channelId
}),
_localMessageUpdateAdapter = UpdateAdapter(
database,
'LocalMessage',
['id'],
(LocalMessage item) => <String, Object?>{
'id': item.id,
'data': _remoteMessageConverter.encode(item.data),
'channelId': item.channelId
});
final sqflite.DatabaseExecutor database;
final StreamController<String> changeListener;
final QueryAdapter _queryAdapter;
final InsertionAdapter<LocalMessage> _localMessageInsertionAdapter;
final UpdateAdapter<LocalMessage> _localMessageUpdateAdapter;
@override
Future<int?> countByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT COUNT(id) FROM LocalMessage WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => row.values.first as int,
arguments: [channelId]);
}
@override
Future<List<LocalMessage>> findAllByChannel(int channelId) async {
return _queryAdapter.queryList(
'SELECT * FROM LocalMessage WHERE channelId = ?1 ORDER BY id DESC',
mapper: (Map<String, Object?> row) => LocalMessage(
row['id'] as int,
_remoteMessageConverter.decode(row['data'] as String),
row['channelId'] as int),
arguments: [channelId]);
}
@override
Future<LocalMessage?> findLastByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT * FROM LocalMessage WHERE channelId = ?1 ORDER BY id DESC LIMIT 1',
mapper: (Map<String, Object?> row) => LocalMessage(row['id'] as int, _remoteMessageConverter.decode(row['data'] as String), row['channelId'] as int),
arguments: [channelId]);
}
@override
Future<void> delete(int id) async {
await _queryAdapter.queryNoReturn('DELETE FROM LocalMessage WHERE id = ?1',
arguments: [id]);
}
@override
Future<List<LocalMessage>> deleteByChannel(int channelId) async {
return _queryAdapter.queryList(
'DELETE FROM LocalMessage WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => LocalMessage(
row['id'] as int,
_remoteMessageConverter.decode(row['data'] as String),
row['channelId'] as int),
arguments: [channelId]);
}
@override
Future<void> wipeLocalMessages() async {
await _queryAdapter.queryNoReturn('DELETE FROM LocalMessage');
}
@override
Future<void> insert(LocalMessage m) async {
await _localMessageInsertionAdapter.insert(m, OnConflictStrategy.replace);
}
@override
Future<void> insertBulk(List<LocalMessage> m) async {
await _localMessageInsertionAdapter.insertList(
m, OnConflictStrategy.replace);
}
@override
Future<void> update(LocalMessage person) async {
await _localMessageUpdateAdapter.update(person, OnConflictStrategy.replace);
}
}
// ignore_for_file: unused_element
final _remoteMessageConverter = RemoteMessageConverter();

View File

@ -27,9 +27,9 @@ class _CallScreenState extends State<CallScreen> {
DateTime.now().difference(provider.current.value!.createdAt); DateTime.now().difference(provider.current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0'); String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = "${twoDigits(duration.inHours)}:" String formattedTime = '${twoDigits(duration.inHours)}:'
"${twoDigits(duration.inMinutes.remainder(60))}:" '${twoDigits(duration.inMinutes.remainder(60))}:'
"${twoDigits(duration.inSeconds.remainder(60))}"; '${twoDigits(duration.inSeconds.remainder(60))}';
return formattedTime; return formattedTime;
} }
@ -66,7 +66,7 @@ class _CallScreenState extends State<CallScreen> {
text: 'call'.tr, text: 'call'.tr,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const TextSpan(text: "\n"), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: currentDuration, text: currentDuration,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,

View File

@ -2,14 +2,14 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/controllers/chat_history_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/call.dart'; import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/models/message.dart'; import 'package:solian/models/message.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart'; import 'package:solian/providers/chat.dart';
import 'package:solian/providers/content/call.dart'; import 'package:solian/providers/content/call.dart';
@ -46,12 +46,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
String? _overrideAlias; String? _overrideAlias;
Channel? _channel; Channel? _channel;
ChannelMember? _channelProfile;
Call? _ongoingCall; Call? _ongoingCall;
ChannelMember? _channelProfile;
StreamSubscription<NetworkPackage>? _subscription; StreamSubscription<NetworkPackage>? _subscription;
final PagingController<int, Message> _pagingController = late final ChatHistoryController _chatController;
PagingController(firstPageKey: 0);
getProfile() async { getProfile() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -106,31 +105,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> getMessages(int pageKey) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/api/channels/${widget.realm}/${widget.alias}/messages?take=10&offset=$pageKey');
if (resp.statusCode == 200) {
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Message.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
} else if (resp.statusCode == 403) {
_pagingController.appendLastPage([]);
} else {
_pagingController.error = resp.bodyString;
}
}
void listenMessages() { void listenMessages() {
final ChatProvider provider = Get.find(); final ChatProvider provider = Get.find();
_subscription = provider.stream.stream.listen((event) { _subscription = provider.stream.stream.listen((event) {
@ -138,33 +112,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
case 'messages.new': case 'messages.new':
final payload = Message.fromJson(event.payload!); final payload = Message.fromJson(event.payload!);
if (payload.channelId == _channel?.id) { if (payload.channelId == _channel?.id) {
final idx = _pagingController.itemList _chatController.receiveMessage(payload);
?.indexWhere((e) => e.uuid == payload.uuid);
if ((idx ?? -1) >= 0) {
_pagingController.itemList?[idx!] = payload;
} else {
_pagingController.itemList?.insert(0, payload);
}
} }
break; break;
case 'messages.update': case 'messages.update':
final payload = Message.fromJson(event.payload!); final payload = Message.fromJson(event.payload!);
if (payload.channelId == _channel?.id) { if (payload.channelId == _channel?.id) {
final idx = _pagingController.itemList _chatController.replaceMessage(payload);
?.indexWhere((x) => x.uuid == payload.uuid);
if (idx != null) {
_pagingController.itemList?[idx] = payload;
}
} }
break; break;
case 'messages.burnt': case 'messages.burnt':
final payload = Message.fromJson(event.payload!); final payload = Message.fromJson(event.payload!);
if (payload.channelId == _channel?.id) { if (payload.channelId == _channel?.id) {
final idx = _pagingController.itemList _chatController.burnMessage(payload.id);
?.indexWhere((x) => x.uuid != payload.uuid);
if (idx != null) {
_pagingController.itemList?.removeAt(idx - 1);
}
} }
break; break;
case 'calls.new': case 'calls.new':
@ -175,7 +135,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_ongoingCall = null; _ongoingCall = null;
break; break;
} }
setState(() {});
}); });
} }
@ -200,24 +159,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Message? _messageToReplying; Message? _messageToReplying;
Message? _messageToEditing; Message? _messageToEditing;
Widget buildHistory(context, Message item, index) { Widget buildHistoryBody(Message item, {bool isMerged = false}) {
bool isMerged = false, hasMerged = false;
if (index > 0) {
hasMerged = checkMessageMergeable(
_pagingController.itemList?[index - 1],
item,
);
}
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
isMerged = checkMessageMergeable(
item,
_pagingController.itemList?[index + 1],
);
}
Widget content;
if (item.replyTo != null) { if (item.replyTo != null) {
content = Column( return Column(
children: [ children: [
ChatMessage( ChatMessage(
key: Key('m${item.replyTo!.uuid}'), key: Key('m${item.replyTo!.uuid}'),
@ -231,17 +175,35 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
], ],
); );
} else { }
content = ChatMessage(
key: Key('m${item.uuid}'), return ChatMessage(
item: item, key: Key('m${item.uuid}'),
isMerged: isMerged, item: item,
isMerged: isMerged,
);
}
Widget buildHistory(context, index) {
bool isMerged = false, hasMerged = false;
if (index > 0) {
hasMerged = checkMessageMergeable(
_chatController.currentHistory[index - 1].data,
_chatController.currentHistory[index].data,
);
}
if (index + 1 < _chatController.currentHistory.length) {
isMerged = checkMessageMergeable(
_chatController.currentHistory[index].data,
_chatController.currentHistory[index + 1].data,
); );
} }
final item = _chatController.currentHistory[index].data;
return InkWell( return InkWell(
child: Container( child: Container(
child: content.paddingOnly( child: buildHistoryBody(item, isMerged: isMerged).paddingOnly(
top: !isMerged ? 8 : 0, top: !isMerged ? 8 : 0,
bottom: !hasMerged ? 8 : 0, bottom: !hasMerged ? 8 : 0,
), ),
@ -268,14 +230,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
@override @override
void initState() { void initState() {
super.initState(); _chatController = ChatHistoryController();
_chatController.initialize();
getChannel().then((_) {
_chatController.getMessages(_channel!, widget.realm);
});
getProfile(); getProfile();
getChannel().then((_) {
listenMessages();
_pagingController.addPageRequestListener(getMessages);
});
getOngoingCall(); getOngoingCall();
listenMessages();
super.initState();
} }
@override @override
@ -352,47 +319,84 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Column( Column(
children: [ children: [
Expanded( Expanded(
child: PagedListView<int, Message>( child: CustomScrollView(
clipBehavior: Clip.none,
reverse: true, reverse: true,
pagingController: _pagingController, slivers: [
builderDelegate: PagedChildBuilderDelegate<Message>( Obx(() {
itemBuilder: buildHistory, return SliverList.builder(
noItemsFoundIndicatorBuilder: (_) => Container(), key: Key('chat-history#${_channel!.id}'),
), itemCount: _chatController.currentHistory.length,
).paddingOnly(bottom: 56), itemBuilder: buildHistory,
);
}),
Obx(() {
final amount = _chatController.totalHistoryCount -
_chatController.currentHistory.length;
if (amount.value <= 0 ||
_chatController.isLoading.isTrue) {
return const SliverToBoxAdapter(child: SizedBox());
}
return SliverToBoxAdapter(
child: ListTile(
tileColor:
Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.sync_disabled),
title: Text('messageUnsync'.tr),
subtitle: Text('messageUnsyncCaption'.trParams({
'count': amount.string,
})),
onTap: () {
_chatController.getMoreMessages(
_channel!,
widget.realm,
);
},
),
);
}),
Obx(() {
if (_chatController.isLoading.isFalse) {
return const SliverToBoxAdapter(child: SizedBox());
}
return SliverToBoxAdapter(
child: const LinearProgressIndicator()
.animate()
.slideY()
.paddingOnly(bottom: 4),
);
}),
],
),
), ),
], ClipRect(
), child: BackdropFilter(
Positioned( filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
bottom: 0, child: SafeArea(
left: 0, child: ChatMessageInput(
right: 0, edit: _messageToEditing,
child: ClipRect( reply: _messageToReplying,
child: BackdropFilter( realm: widget.realm,
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), placeholder: placeholder,
child: SafeArea( channel: _channel!,
child: ChatMessageInput( onSent: (Message item) {
edit: _messageToEditing, setState(() {
reply: _messageToReplying, _chatController.addTemporaryMessage(item);
realm: widget.realm, });
placeholder: placeholder, },
channel: _channel!, onReset: () {
onSent: (Message item) { setState(() {
setState(() { _messageToReplying = null;
_pagingController.itemList?.insert(0, item); _messageToEditing = null;
}); });
}, },
onReset: () { ),
setState(() {
_messageToReplying = null;
_messageToEditing = null;
});
},
), ),
), ),
), ),
), ],
), ),
if (_ongoingCall != null) if (_ongoingCall != null)
Positioned( Positioned(

View File

@ -101,7 +101,7 @@ class _ChatScreenState extends State<ChatScreen> {
context), context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: AppBarTitle('chat'.tr), title: AppBarTitle('chat'.tr),
centerTitle: true, centerTitle: false,
floating: true, floating: true,
titleSpacing: SolianTheme.titleSpacing(context), titleSpacing: SolianTheme.titleSpacing(context),
toolbarHeight: SolianTheme.toolbarHeight(context), toolbarHeight: SolianTheme.toolbarHeight(context),

View File

@ -164,6 +164,8 @@ class SolianMessages extends Translations {
'channelNotifyLevelMentioned': 'Only mentioned', 'channelNotifyLevelMentioned': 'Only mentioned',
'channelNotifyLevelNone': 'Ignore all', 'channelNotifyLevelNone': 'Ignore all',
'channelNotifyLevelApplied': 'Your notification settings has been applied.', 'channelNotifyLevelApplied': 'Your notification settings has been applied.',
'messageUnsync': 'Messages Un-synced',
'messageUnsyncCaption': '@count message(s) still in un-synced.',
'messageDecoding': 'Decoding...', 'messageDecoding': 'Decoding...',
'messageDecodeFailed': 'Unable to decode: @message', 'messageDecodeFailed': 'Unable to decode: @message',
'messageInputPlaceholder': 'Message @channel', 'messageInputPlaceholder': 'Message @channel',
@ -364,6 +366,8 @@ class SolianMessages extends Translations {
'channelNotifyLevelMentioned': '仅提及', 'channelNotifyLevelMentioned': '仅提及',
'channelNotifyLevelNone': '忽略一切', 'channelNotifyLevelNone': '忽略一切',
'channelNotifyLevelApplied': '你的通知设置已经应用。', 'channelNotifyLevelApplied': '你的通知设置已经应用。',
'messageUnsync': '消息未同步',
'messageUnsyncCaption': '还有 @count 条消息未同步',
'messageDecoding': '解码信息中…', 'messageDecoding': '解码信息中…',
'messageDecodeFailed': '解码信息失败:@message', 'messageDecodeFailed': '解码信息失败:@message',
'messageInputPlaceholder': '在 @channel 发信息', 'messageInputPlaceholder': '在 @channel 发信息',

View File

@ -67,7 +67,7 @@ class SliverFriendList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverList.builder( return SliverList.builder(
itemCount: items.length, itemCount: items.length,
itemBuilder: (_, __) => buildItem(_, __), itemBuilder: (context, idx) => buildItem(context, idx),
); );
} }
} }

View File

@ -65,7 +65,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
? widget.realm?.alias ? widget.realm?.alias
: 'global'; : 'global';
final resp = await client final resp = await client
.delete('/api/channels/${scope}/${widget.channel.alias}/calls/ongoing'); .delete('/api/channels/$scope/${widget.channel.alias}/calls/ongoing');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
if (widget.onEnded != null) widget.onEnded!(); if (widget.onEnded != null) widget.onEnded!();
} else { } else {

View File

@ -98,7 +98,7 @@ class _PostItemState extends State<PostItem> {
if (labels.isNotEmpty) { if (labels.isNotEmpty) {
return Text( return Text(
labels.join(" · "), labels.join(' · '),
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,

View File

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
version: "67.0.0"
_flutterfire_internals: _flutterfire_internals:
dependency: transitive dependency: transitive
description: description:
@ -9,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.37" version: "1.3.37"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -41,6 +57,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
build:
dependency: transitive
description:
name: build
sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
build_config:
dependency: transitive
description:
name: build_config
sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
url: "https://pub.dev"
source: hosted
version: "1.1.1"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
url: "https://pub.dev"
source: hosted
version: "2.4.11"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
url: "https://pub.dev"
source: hosted
version: "7.3.1"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
url: "https://pub.dev"
source: hosted
version: "8.9.2"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
@ -81,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted
version: "1.3.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -113,6 +201,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
url: "https://pub.dev"
source: hosted
version: "4.10.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -137,6 +233,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -169,6 +273,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
dart_webrtc: dart_webrtc:
dependency: transitive dependency: transitive
description: description:
@ -185,6 +297,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.10"
dev_build:
dependency: transitive
description:
name: dev_build
sha256: "4a4c8e3aaaaa9a7de4039dc711ee573de5612a35a4469d01ecf30e5ae74cbcfd"
url: "https://pub.dev"
source: hosted
version: "0.16.7+4"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -329,6 +449,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
floor:
dependency: "direct main"
description:
name: floor
sha256: c1b06023912b5b8e49deb6a9d867863c535ae1a232d991c3582bba3ee8687867
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_annotation:
dependency: transitive
description:
name: floor_annotation
sha256: a40949580a7ab0eee572686e2d3b1638fd6bd6a753e661d792ab4236b365b23b
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_common:
dependency: transitive
description:
name: floor_common
sha256: "41c9914862f83a821815e1b1ffd47a1e1ae2130c35ff882ba2d000a67713ba64"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
floor_generator:
dependency: "direct dev"
description:
name: floor_generator
sha256: "1499b3ab878a807e6fbe6f140dc014124845cd1df3090a113aae5fa7577a1e77"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -504,6 +656,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.7.0" version: "10.7.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:
@ -512,6 +672,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.6.6" version: "4.6.6"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
go_router: go_router:
dependency: "direct main" dependency: "direct main"
description: description:
@ -520,6 +688,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.0" version: "14.2.0"
graphs:
dependency: transitive
description:
name: graphs
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
url: "https://pub.dev"
source: hosted
version: "2.3.1"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -536,6 +712,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -632,6 +816,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -680,6 +872,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
lists:
dependency: transitive
description:
name: lists
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
livekit_client: livekit_client:
dependency: "direct main" dependency: "direct main"
description: description:
@ -776,6 +976,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -928,6 +1136,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
process_run:
dependency: transitive
description:
name: process_run
sha256: "8d9c6198b98fbbfb511edd42e7364e24d85c163e47398919871b952dc86a423e"
url: "https://pub.dev"
source: hosted
version: "0.14.2"
protobuf: protobuf:
dependency: transitive dependency: transitive
description: description:
@ -952,6 +1176,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
url: "https://pub.dev"
source: hosted
version: "1.3.0"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1040,6 +1272,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
shelf:
dependency: transitive
description:
name: shelf
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
url: "https://pub.dev"
source: hosted
version: "1.4.1"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -1053,6 +1301,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.12" version: "0.2.12"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -1070,7 +1326,7 @@ packages:
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
sqflite: sqflite:
dependency: transitive dependency: "direct main"
description: description:
name: sqflite name: sqflite
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
@ -1085,6 +1341,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4" version: "2.5.4"
sqflite_common_ffi:
dependency: "direct dev"
description:
name: sqflite_common_ffi
sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
sqflite_common_ffi_web:
dependency: "direct dev"
description:
name: sqflite_common_ffi_web
sha256: cfc9d1c61a3e06e5b2e96994a44b11125b4f451fee95b9fad8bd473b4613d592
url: "https://pub.dev"
source: hosted
version: "0.4.3+1"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
url: "https://pub.dev"
source: hosted
version: "2.4.3"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "7b20045d1ccfb7bc1df7e8f9fee5ae58673fce6ff62cefbb0e0fd7214e90e5a0"
url: "https://pub.dev"
source: hosted
version: "0.34.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -1101,6 +1389,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -1109,6 +1405,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
strings:
dependency: transitive
description:
name: strings
sha256: b33f40c4dd3e597bf6d9e7f4f4dc282dad0f19b07d9f320cb5c2183859cbccf5
url: "https://pub.dev"
source: hosted
version: "3.1.1"
synchronized: synchronized:
dependency: transitive dependency: transitive
description: description:
@ -1149,6 +1453,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3" version: "0.9.3"
timing:
dependency: transitive
description:
name: timing
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -1157,6 +1469,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
unicode:
dependency: transitive
description:
name: unicode
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1309,6 +1629,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,38 +1,16 @@
name: solian name: solian
description: "The Solar Network App" description: "The Solar Network App"
# The following line prevents the package from being accidentally published to publish_to: "none"
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.0+1 version: 1.1.0+1
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
go_router: ^14.1.1 go_router: ^14.1.1
get: ^4.6.6 get: ^4.6.6
@ -70,59 +48,27 @@ dependencies:
device_info_plus: ^10.1.0 device_info_plus: ^10.1.0
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
flutter_acrylic: ^1.1.4 flutter_acrylic: ^1.1.4
floor: ^1.5.0
sqflite: ^2.3.3+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0 flutter_lints: ^4.0.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
# For information on the generic Dart part of this file, see the floor_generator: ^1.4.0
# following page: https://dart.dev/tools/pub/pubspec build_runner: ^2.1.2
sqflite_common_ffi: ^2.3.3
sqflite_common_ffi_web: ^0.4.3+1
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets: assets:
- assets/logo.png - assets/logo.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true