Add user avatars
This commit is contained in:
parent
7b776db632
commit
8a3ff70523
11 changed files with 415 additions and 85 deletions
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:contacts_plus/models/message.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
@ -7,7 +8,11 @@ import 'package:contacts_plus/models/authentication_data.dart';
|
|||
import 'package:signalr_netcore/http_connection_options.dart';
|
||||
import 'package:signalr_netcore/hub_connection.dart';
|
||||
import 'package:signalr_netcore/hub_connection_builder.dart';
|
||||
import 'package:signalr_netcore/ihub_protocol.dart';
|
||||
import 'package:signalr_netcore/msgpack_hub_protocol.dart';
|
||||
import 'package:signalr_netcore/web_supporting_http_client.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'config.dart';
|
||||
|
||||
|
@ -17,21 +22,17 @@ class ApiClient {
|
|||
static const String tokenKey = "token";
|
||||
static const String passwordKey = "password";
|
||||
|
||||
static final ApiClient _singleton = ApiClient._internal();
|
||||
|
||||
factory ApiClient() {
|
||||
return _singleton;
|
||||
ApiClient({required AuthenticationData authenticationData}) : _authenticationData = authenticationData {
|
||||
if (_authenticationData.isAuthenticated) {
|
||||
hub.start();
|
||||
}
|
||||
}
|
||||
|
||||
ApiClient._internal();
|
||||
late final NeosHub hub = NeosHub(token: authorizationHeader.values.first);
|
||||
final AuthenticationData _authenticationData;
|
||||
|
||||
final NeosHub _hub = NeosHub();
|
||||
AuthenticationData? _authenticationData;
|
||||
|
||||
set authenticationData(value) => _authenticationData = value;
|
||||
|
||||
String get userId => _authenticationData!.userId;
|
||||
bool get isAuthenticated => _authenticationData?.isAuthenticated ?? false;
|
||||
String get userId => _authenticationData.userId;
|
||||
bool get isAuthenticated => _authenticationData.isAuthenticated;
|
||||
|
||||
static Future<AuthenticationData> tryLogin({
|
||||
required String username,
|
||||
|
@ -104,7 +105,7 @@ class ApiClient {
|
|||
}
|
||||
|
||||
Map<String, String> get authorizationHeader => {
|
||||
"Authorization": "neos ${_authenticationData!.userId}:${_authenticationData!.token}"
|
||||
"Authorization": "neos ${_authenticationData.userId}:${_authenticationData.token}"
|
||||
};
|
||||
|
||||
static Uri buildFullUri(String path) => Uri.parse("${Config.apiBaseUrl}/api$path");
|
||||
|
@ -136,19 +137,38 @@ class ApiClient {
|
|||
}
|
||||
|
||||
class NeosHub {
|
||||
final HubConnection hubConnection;
|
||||
late final Future<void>? _hubConnectedFuture;
|
||||
late final HubConnection hubConnection;
|
||||
final Logger _logger = Logger("NeosHub");
|
||||
|
||||
NeosHub() : hubConnection = HubConnectionBuilder()
|
||||
.withUrl(Config.neosHubUrl, options: HttpConnectionOptions())
|
||||
.withAutomaticReconnect()
|
||||
.build() {
|
||||
_hubConnectedFuture = hubConnection.start()?.whenComplete(() {
|
||||
NeosHub({required String token}) {
|
||||
hubConnection = HubConnectionBuilder()
|
||||
.withUrl(
|
||||
Config.neosHubUrl,
|
||||
options: HttpConnectionOptions(
|
||||
headers: MessageHeaders()
|
||||
..setHeaderValue("Authorization", token),
|
||||
httpClient: WebSupportingHttpClient(
|
||||
_logger,
|
||||
),
|
||||
logger: _logger,
|
||||
logMessageContent: true
|
||||
),
|
||||
).withAutomaticReconnect().build();
|
||||
hubConnection.onreconnecting(({error}) {
|
||||
log("onreconnecting called with error $error");
|
||||
});
|
||||
hubConnection.onreconnected(({connectionId}) {
|
||||
log("onreconnected called");
|
||||
});
|
||||
}
|
||||
|
||||
void start() {
|
||||
hubConnection.start()?.onError((error, stackTrace) => log(error.toString())).whenComplete(() {
|
||||
log("Hub connection established");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BaseClient {
|
||||
static final client = ApiClient();
|
||||
Future<void> sendMessage(Message message) async {
|
||||
await hubConnection.send("SendMessage", args: [message.toMap()]);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ import 'dart:convert';
|
|||
import 'package:contacts_plus/api_client.dart';
|
||||
import 'package:contacts_plus/models/friend.dart';
|
||||
|
||||
class FriendApi extends BaseClient {
|
||||
static Future<Iterable<Friend>> getFriendsList() async {
|
||||
final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/friends");
|
||||
class FriendApi {
|
||||
static Future<Iterable<Friend>> getFriendsList(ApiClient client) async {
|
||||
final response = await client.get("/users/${client.userId}/friends");
|
||||
ApiClient.checkResponse(response);
|
||||
final data = jsonDecode(response.body) as List;
|
||||
return data.map((e) => Friend.fromMap(e));
|
||||
|
|
|
@ -3,9 +3,9 @@ import 'dart:convert';
|
|||
import 'package:contacts_plus/api_client.dart';
|
||||
import 'package:contacts_plus/models/message.dart';
|
||||
|
||||
class MessageApi extends BaseClient {
|
||||
static Future<Iterable<Message>> getUserMessages({String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async {
|
||||
final response = await BaseClient.client.get("/users/${BaseClient.client.userId}/messages"
|
||||
class MessageApi {
|
||||
static Future<Iterable<Message>> getUserMessages(ApiClient client, {String userId="", DateTime? fromTime, int maxItems=50, bool unreadOnly=false}) async {
|
||||
final response = await client.get("/users/${client.userId}/messages"
|
||||
"?maxItems=$maxItems"
|
||||
"${fromTime == null ? "" : "&fromTime${fromTime.toLocal().toIso8601String()}"}"
|
||||
"${userId.isEmpty ? "" : "&user=$userId"}"
|
||||
|
|
|
@ -5,50 +5,62 @@ import 'api_client.dart';
|
|||
import 'models/authentication_data.dart';
|
||||
|
||||
void main() {
|
||||
runApp(ContactsPlus());
|
||||
runApp(const ContactsPlus());
|
||||
}
|
||||
|
||||
class ContactsPlus extends StatelessWidget {
|
||||
ContactsPlus({super.key});
|
||||
class ContactsPlus extends StatefulWidget {
|
||||
const ContactsPlus({super.key});
|
||||
|
||||
@override
|
||||
State<ContactsPlus> createState() => _ContactsPlusState();
|
||||
}
|
||||
|
||||
class _ContactsPlusState extends State<ContactsPlus> {
|
||||
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
||||
AuthenticationData _authData = AuthenticationData.unauthenticated();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
return ClientHolder(
|
||||
authenticationData: _authData,
|
||||
child: MaterialApp(
|
||||
title: 'Contacts+',
|
||||
theme: ThemeData(
|
||||
textTheme: _typography.white,
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark)
|
||||
),
|
||||
home: const SplashScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
final ApiClient _apiClient = ApiClient();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_apiClient.isAuthenticated) {
|
||||
return const HomeScreen();
|
||||
} else {
|
||||
return LoginScreen(
|
||||
home: _authData.isAuthenticated ?
|
||||
const HomeScreen() :
|
||||
LoginScreen(
|
||||
onLoginSuccessful: (AuthenticationData authData) {
|
||||
if (authData.isAuthenticated) {
|
||||
setState(() {
|
||||
_apiClient.authenticationData = authData;
|
||||
_authData = authData;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientHolder extends InheritedWidget {
|
||||
final ApiClient client;
|
||||
|
||||
ClientHolder({super.key, required AuthenticationData authenticationData, required super.child})
|
||||
: client = ApiClient(authenticationData: authenticationData);
|
||||
|
||||
static ClientHolder? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<ClientHolder>();
|
||||
}
|
||||
|
||||
static ClientHolder of(BuildContext context) {
|
||||
final ClientHolder? result = maybeOf(context);
|
||||
assert(result != null, 'No AuthenticatedClient found in context');
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant ClientHolder oldWidget) => oldWidget.client != client;
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:contacts_plus/models/user_profile.dart';
|
||||
|
||||
class Friend extends Comparable {
|
||||
final String id;
|
||||
final String username;
|
||||
final UserStatus userStatus;
|
||||
final UserProfile userProfile;
|
||||
|
||||
Friend({required this.id, required this.username, required this.userStatus});
|
||||
Friend({required this.id, required this.username, required this.userStatus, required this.userProfile});
|
||||
|
||||
factory Friend.fromMap(Map map) {
|
||||
return Friend(id: map["id"], username: map["friendUsername"], userStatus: UserStatus.fromMap(map["userStatus"]));
|
||||
return Friend(
|
||||
id: map["id"],
|
||||
username: map["friendUsername"],
|
||||
userStatus: UserStatus.fromMap(map["userStatus"]),
|
||||
userProfile: UserProfile.fromMap(map["profile"] ?? {"iconUrl": ""}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,11 +1,30 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum MessageType {
|
||||
unknown,
|
||||
text,
|
||||
sound,
|
||||
sessionInvite,
|
||||
object,
|
||||
object;
|
||||
|
||||
static const Map<MessageType, String> _mapper = {
|
||||
MessageType.text: "Text",
|
||||
MessageType.sound: "Sound",
|
||||
MessageType.sessionInvite: "SessionInvite",
|
||||
MessageType.object: "Object",
|
||||
};
|
||||
|
||||
factory MessageType.fromName(String name) {
|
||||
return MessageType.values.firstWhere((element) => element.name.toLowerCase() == name.toLowerCase(),
|
||||
orElse: () => MessageType.unknown,
|
||||
);
|
||||
}
|
||||
|
||||
String? toName() {
|
||||
return _mapper[this];
|
||||
}
|
||||
}
|
||||
|
||||
class Message {
|
||||
|
@ -20,11 +39,9 @@ class Message {
|
|||
required this.content, required this.sendTime});
|
||||
|
||||
factory Message.fromMap(Map map) {
|
||||
final typeString = map["messageType"] as String?;
|
||||
final type = MessageType.values.firstWhere((element) => element.name.toLowerCase() == typeString?.toLowerCase(),
|
||||
orElse: () => MessageType.unknown,
|
||||
);
|
||||
if (type == MessageType.unknown && typeString != null) {
|
||||
final typeString = (map["messageType"] as String?) ?? "";
|
||||
final type = MessageType.fromName(typeString);
|
||||
if (type == MessageType.unknown && typeString.isNotEmpty) {
|
||||
log("Unknown MessageType '$typeString' in response");
|
||||
}
|
||||
return Message(
|
||||
|
@ -36,4 +53,18 @@ class Message {
|
|||
sendTime: DateTime.parse(map["sendTime"]),
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap() => {
|
||||
"id": id,
|
||||
"recipientId": recipientId,
|
||||
"senderId": senderId,
|
||||
"ownerId": senderId,
|
||||
"messageType": type.toName(),
|
||||
"content": content,
|
||||
"sendTime": sendTime.toIso8601String(),
|
||||
};
|
||||
|
||||
static String generateId() {
|
||||
return "MSG-${const Uuid().v4()}";
|
||||
}
|
||||
}
|
20
lib/models/user_profile.dart
Normal file
20
lib/models/user_profile.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'package:contacts_plus/config.dart';
|
||||
|
||||
class UserProfile {
|
||||
final String iconUrl;
|
||||
|
||||
UserProfile({required this.iconUrl});
|
||||
|
||||
factory UserProfile.fromMap(Map map) {
|
||||
return UserProfile(iconUrl: map["iconUrl"]);
|
||||
}
|
||||
|
||||
Uri get httpIconUri {
|
||||
final fullUri = iconUrl.replaceFirst("neosdb:///", Config.neosCdnUrl);
|
||||
final lastPeriodIndex = fullUri.lastIndexOf(".");
|
||||
if (lastPeriodIndex != -1 && fullUri.length - lastPeriodIndex < 8) {
|
||||
return Uri.parse(fullUri.substring(0, lastPeriodIndex));
|
||||
}
|
||||
return Uri.parse(fullUri);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:contacts_plus/apis/friend_api.dart';
|
||||
import 'package:contacts_plus/aux.dart';
|
||||
import 'package:contacts_plus/main.dart';
|
||||
import 'package:contacts_plus/models/friend.dart';
|
||||
import 'package:contacts_plus/widgets/messages.dart';
|
||||
|
@ -13,15 +15,20 @@ class HomeScreen extends StatefulWidget {
|
|||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
Future<List<Friend>>? _friendsFuture;
|
||||
ClientHolder? _clientHolder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
if (_clientHolder != clientHolder) {
|
||||
_clientHolder = clientHolder;
|
||||
_refreshFriendsList();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshFriendsList() {
|
||||
_friendsFuture = FriendApi.getFriendsList().then((Iterable<Friend> value) =>
|
||||
_friendsFuture = FriendApi.getFriendsList(_clientHolder!.client).then((Iterable<Friend> value) =>
|
||||
value.toList()
|
||||
..sort((a, b) {
|
||||
if (a.userStatus.onlineStatus == b.userStatus.onlineStatus) {
|
||||
|
@ -40,6 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final apiClient = ClientHolder.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Contacts+"),
|
||||
|
@ -58,11 +66,27 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = data.elementAt(index);
|
||||
final iconUri = entry.userProfile.httpIconUri.toString();
|
||||
return ListTile(
|
||||
leading: CachedNetworkImage(
|
||||
imageBuilder: (context, imageProvider) {
|
||||
return CircleAvatar(
|
||||
foregroundImage: imageProvider,
|
||||
);
|
||||
},
|
||||
imageUrl: iconUri,
|
||||
placeholder: (context, url) {
|
||||
return const CircleAvatar(backgroundColor: Colors.white54,);
|
||||
},
|
||||
errorWidget: (context, error, what) => const CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
title: Text(entry.username),
|
||||
subtitle: Text(entry.userStatus.onlineStatus.name),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => Messages(friend: entry)));
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => Messages(friend: entry)));
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:contacts_plus/api_client.dart';
|
||||
import 'package:contacts_plus/apis/message_api.dart';
|
||||
import 'package:contacts_plus/main.dart';
|
||||
import 'package:contacts_plus/models/friend.dart';
|
||||
import 'package:contacts_plus/models/message.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -18,22 +18,27 @@ class Messages extends StatefulWidget {
|
|||
class _MessagesState extends State<Messages> {
|
||||
Future<Iterable<Message>>? _messagesFuture;
|
||||
final TextEditingController _messageTextController = TextEditingController();
|
||||
ClientHolder? _clientHolder;
|
||||
|
||||
bool _isSendable = false;
|
||||
|
||||
void _refreshMessages() {
|
||||
_messagesFuture = MessageApi.getUserMessages(userId: widget.friend.id)..then((value) => value.toList());
|
||||
_messagesFuture = MessageApi.getUserMessages(_clientHolder!.client, userId: widget.friend.id)..then((value) => value.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
if (_clientHolder != clientHolder) {
|
||||
_clientHolder = clientHolder;
|
||||
_refreshMessages();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final apiClient = ApiClient();
|
||||
final apiClient = ClientHolder.of(context).client;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.friend.username),
|
||||
|
@ -110,7 +115,33 @@ class _MessagesState extends State<Messages> {
|
|||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: IconButton(
|
||||
splashRadius: 24,
|
||||
onPressed: _isSendable ? () {} : null,
|
||||
onPressed: _isSendable ? () async {
|
||||
setState(() {
|
||||
_isSendable = false;
|
||||
});
|
||||
final message = Message(
|
||||
id: Message.generateId(),
|
||||
recipientId: widget.friend.id,
|
||||
senderId: apiClient.userId, type: MessageType.text,
|
||||
content: _messageTextController.text,
|
||||
sendTime: DateTime.now().toUtc(),
|
||||
);
|
||||
try {
|
||||
await apiClient.hub.sendMessage(message);
|
||||
_messageTextController.clear();
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Failed to send message\n$e",
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isSendable = true;
|
||||
});
|
||||
}
|
||||
} : null,
|
||||
iconSize: 28,
|
||||
icon: const Icon(Icons.send),
|
||||
),
|
||||
|
|
188
pubspec.lock
188
pubspec.lock
|
@ -17,6 +17,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -65,11 +89,43 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_blurhash
|
||||
sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -177,7 +233,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
|
||||
|
@ -216,6 +272,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -224,6 +288,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.14"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.10"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -232,6 +360,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
signalr_netcore:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -253,6 +397,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "8453780d1f703ead201a39673deb93decf85d543f359f750e2afc4908b55533f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.8"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.5"
|
||||
sse_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -285,6 +445,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -341,6 +509,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
sdks:
|
||||
dart: ">=2.19.6 <3.0.0"
|
||||
flutter: ">=2.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
|
|
@ -41,6 +41,8 @@ dependencies:
|
|||
intl: ^0.18.1
|
||||
path: ^1.8.2
|
||||
signalr_netcore: ^1.3.3
|
||||
logging: ^1.1.1
|
||||
cached_network_image: ^3.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue