channel and message lists
This commit is contained in:
parent
1286a5d848
commit
56d49d8c5e
@ -23,6 +23,7 @@ linter:
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
prefer_relative_imports: true,
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
@ -1,8 +1,23 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:simplecloudnotifier/models/key_token_auth.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
enum ChannelSelector {
|
||||
owned(apiKey: 'owned'), // Return all channels of the user
|
||||
subscribedAny(apiKey: 'subscribed_any'), // Return all channels that the user is subscribing to
|
||||
allAny(apiKey: 'all_any'), // Return channels that the user owns or is subscribing
|
||||
subscribed(apiKey: 'subscribed'), // Return all channels that the user is subscribing to (even unconfirmed)
|
||||
all(apiKey: 'all'); // Return channels that the user owns or is subscribing (even unconfirmed)
|
||||
|
||||
const ChannelSelector({required this.apiKey});
|
||||
final String apiKey;
|
||||
}
|
||||
|
||||
class APIClient {
|
||||
static const String _base = 'https://simplecloudnotifier.de/api/v2';
|
||||
|
||||
@ -23,4 +38,41 @@ class APIClient {
|
||||
|
||||
return User.fromJson(jsonDecode(response.body));
|
||||
}
|
||||
|
||||
static getChannelList(KeyTokenAuth auth, ChannelSelector sel) async {
|
||||
var url = '$_base/users/${auth.userId}/channels?selector=${sel.apiKey}';
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('API request failed');
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
return data['channels'].map<ChannelWithSubscription>((e) => ChannelWithSubscription.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
static getMessageList(KeyTokenAuth auth, String pageToken, int? pageSize) async {
|
||||
var url = '$_base/messages?next_page_token=$pageToken';
|
||||
if (pageSize != null) {
|
||||
url += '&page_size=$pageSize';
|
||||
}
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('API request failed');
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
final npt = data['next_page_token'] as String;
|
||||
|
||||
final messages = data['messages'].map<Message>((e) => Message.fromJson(e)).toList();
|
||||
|
||||
return [npt, messages];
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import 'package:flutter/material.dart';
|
||||
class FabWithIcons extends StatefulWidget {
|
||||
FabWithIcons({super.key, required this.icons, required this.onIconTapped});
|
||||
final List<IconData> icons;
|
||||
ValueChanged<int> onIconTapped;
|
||||
final ValueChanged<int> onIconTapped;
|
||||
|
||||
@override
|
||||
State createState() => FabWithIconsState();
|
||||
}
|
||||
|
100
flutter/lib/models/channel.dart
Normal file
100
flutter/lib/models/channel.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
|
||||
class Channel {
|
||||
final String channelID;
|
||||
final String ownerUserID;
|
||||
final String internalName;
|
||||
final String displayName;
|
||||
final String? descriptionName;
|
||||
final String? subscribeKey;
|
||||
final String timestampCreated;
|
||||
final String? timestampLastSent;
|
||||
final int messagesSent;
|
||||
|
||||
const Channel({
|
||||
required this.channelID,
|
||||
required this.ownerUserID,
|
||||
required this.internalName,
|
||||
required this.displayName,
|
||||
required this.descriptionName,
|
||||
required this.subscribeKey,
|
||||
required this.timestampCreated,
|
||||
required this.timestampLastSent,
|
||||
required this.messagesSent,
|
||||
});
|
||||
|
||||
factory Channel.fromJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
'channel_id': String channelID,
|
||||
'owner_user_id': String ownerUserID,
|
||||
'internal_name': String internalName,
|
||||
'display_name': String displayName,
|
||||
'description_name': String? descriptionName,
|
||||
'subscribe_key': String? subscribeKey,
|
||||
'timestamp_created': String timestampCreated,
|
||||
'timestamp_lastsent': String? timestampLastSent,
|
||||
'messages_sent': int messagesSent,
|
||||
} =>
|
||||
Channel(
|
||||
channelID: channelID,
|
||||
ownerUserID: ownerUserID,
|
||||
internalName: internalName,
|
||||
displayName: displayName,
|
||||
descriptionName: descriptionName,
|
||||
subscribeKey: subscribeKey,
|
||||
timestampCreated: timestampCreated,
|
||||
timestampLastSent: timestampLastSent,
|
||||
messagesSent: messagesSent,
|
||||
),
|
||||
_ => throw const FormatException('Failed to decode Channel.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelWithSubscription extends Channel {
|
||||
final Subscription subscription;
|
||||
|
||||
ChannelWithSubscription({
|
||||
required super.channelID,
|
||||
required super.ownerUserID,
|
||||
required super.internalName,
|
||||
required super.displayName,
|
||||
required super.descriptionName,
|
||||
required super.subscribeKey,
|
||||
required super.timestampCreated,
|
||||
required super.timestampLastSent,
|
||||
required super.messagesSent,
|
||||
required this.subscription,
|
||||
});
|
||||
|
||||
factory ChannelWithSubscription.fromJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
'channel_id': String channelID,
|
||||
'owner_user_id': String ownerUserID,
|
||||
'internal_name': String internalName,
|
||||
'display_name': String displayName,
|
||||
'description_name': String? descriptionName,
|
||||
'subscribe_key': String? subscribeKey,
|
||||
'timestamp_created': String timestampCreated,
|
||||
'timestamp_lastsent': String? timestampLastSent,
|
||||
'messages_sent': int messagesSent,
|
||||
'subscription': dynamic subscription,
|
||||
} =>
|
||||
ChannelWithSubscription(
|
||||
channelID: channelID,
|
||||
ownerUserID: ownerUserID,
|
||||
internalName: internalName,
|
||||
displayName: displayName,
|
||||
descriptionName: descriptionName,
|
||||
subscribeKey: subscribeKey,
|
||||
timestampCreated: timestampCreated,
|
||||
timestampLastSent: timestampLastSent,
|
||||
messagesSent: messagesSent,
|
||||
subscription: Subscription.fromJson(subscription),
|
||||
),
|
||||
_ => throw const FormatException('Failed to decode Channel.'),
|
||||
};
|
||||
}
|
||||
}
|
67
flutter/lib/models/message.dart
Normal file
67
flutter/lib/models/message.dart
Normal file
@ -0,0 +1,67 @@
|
||||
class Message {
|
||||
final String messageID;
|
||||
final String senderUserID;
|
||||
final String channelInternalName;
|
||||
final String channelID;
|
||||
final String? senderName;
|
||||
final String senderIP;
|
||||
final String timestamp;
|
||||
final String title;
|
||||
final String? content;
|
||||
final int priority;
|
||||
final String? userMessageID;
|
||||
final String usedKeyID;
|
||||
final bool trimmed;
|
||||
|
||||
const Message({
|
||||
required this.messageID,
|
||||
required this.senderUserID,
|
||||
required this.channelInternalName,
|
||||
required this.channelID,
|
||||
required this.senderName,
|
||||
required this.senderIP,
|
||||
required this.timestamp,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.priority,
|
||||
required this.userMessageID,
|
||||
required this.usedKeyID,
|
||||
required this.trimmed,
|
||||
});
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
'message_id': String messageID,
|
||||
'sender_user_id': String senderUserID,
|
||||
'channel_internal_name': String channelInternalName,
|
||||
'channel_id': String channelID,
|
||||
'sender_name': String? senderName,
|
||||
'sender_ip': String senderIP,
|
||||
'timestamp': String timestamp,
|
||||
'title': String title,
|
||||
'content': String? content,
|
||||
'priority': int priority,
|
||||
'usr_message_id': String? userMessageID,
|
||||
'used_key_id': String usedKeyID,
|
||||
'trimmed': bool trimmed,
|
||||
} =>
|
||||
Message(
|
||||
messageID: messageID,
|
||||
senderUserID: senderUserID,
|
||||
channelInternalName: channelInternalName,
|
||||
channelID: channelID,
|
||||
senderName: senderName,
|
||||
senderIP: senderIP,
|
||||
timestamp: timestamp,
|
||||
title: title,
|
||||
content: content,
|
||||
priority: priority,
|
||||
userMessageID: userMessageID,
|
||||
usedKeyID: usedKeyID,
|
||||
trimmed: trimmed,
|
||||
),
|
||||
_ => throw const FormatException('Failed to decode Message.'),
|
||||
};
|
||||
}
|
||||
}
|
43
flutter/lib/models/subscription.dart
Normal file
43
flutter/lib/models/subscription.dart
Normal file
@ -0,0 +1,43 @@
|
||||
class Subscription {
|
||||
final String subscriptionID;
|
||||
final String subscriberUserID;
|
||||
final String channelOwnerUserID;
|
||||
final String channelID;
|
||||
final String channelInternalName;
|
||||
final String timestampCreated;
|
||||
final bool confirmed;
|
||||
|
||||
const Subscription({
|
||||
required this.subscriptionID,
|
||||
required this.subscriberUserID,
|
||||
required this.channelOwnerUserID,
|
||||
required this.channelID,
|
||||
required this.channelInternalName,
|
||||
required this.timestampCreated,
|
||||
required this.confirmed,
|
||||
});
|
||||
|
||||
factory Subscription.fromJson(Map<String, dynamic> json) {
|
||||
return switch (json) {
|
||||
{
|
||||
'subscription_id': String subscriptionID,
|
||||
'subscriber_user_id': String subscriberUserID,
|
||||
'channel_owner_user_id': String channelOwnerUserID,
|
||||
'channel_id': String channelID,
|
||||
'channel_internal_name': String channelInternalName,
|
||||
'timestamp_created': String timestampCreated,
|
||||
'confirmed': bool confirmed,
|
||||
} =>
|
||||
Subscription(
|
||||
subscriptionID: subscriptionID,
|
||||
subscriberUserID: subscriberUserID,
|
||||
channelOwnerUserID: channelOwnerUserID,
|
||||
channelID: channelID,
|
||||
channelInternalName: channelInternalName,
|
||||
timestampCreated: timestampCreated,
|
||||
confirmed: confirmed,
|
||||
),
|
||||
_ => throw const FormatException('Failed to decode Subscription.'),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/root.dart';
|
||||
import 'package:simplecloudnotifier/pages/send/root.dart';
|
||||
|
||||
import 'bottom_fab/fab_bottom_app_bar.dart';
|
||||
import 'pages/account/root.dart';
|
||||
import 'pages/message_list/message_list.dart';
|
||||
import 'pages/settings/root.dart';
|
||||
import 'state/app_theme.dart';
|
||||
|
||||
class SCNNavLayout extends StatefulWidget {
|
||||
@ -19,10 +21,10 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
int _selectedIndex = 0; // 4 == FAB
|
||||
|
||||
static const List<Widget> _subPages = <Widget>[
|
||||
MessageListPage(title: 'Messages'),
|
||||
MessageListPage(title: 'Page 2'),
|
||||
MessageListPage(),
|
||||
ChannelRootPage(),
|
||||
AccountRootPage(),
|
||||
MessageListPage(title: 'Page 4'),
|
||||
SettingsRootPage(),
|
||||
SendRootPage(),
|
||||
];
|
||||
|
||||
|
@ -33,12 +33,13 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 250,
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _ctrlUserID,
|
||||
decoration: const InputDecoration(
|
||||
@ -48,8 +49,8 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _ctrlToken,
|
||||
decoration: const InputDecoration(
|
||||
|
18
flutter/lib/pages/channel_list/channel_list_item.dart
Normal file
18
flutter/lib/pages/channel_list/channel_list_item.dart
Normal file
@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/channel.dart';
|
||||
|
||||
class ChannelListItem extends StatelessWidget {
|
||||
const ChannelListItem({
|
||||
required this.channel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Channel channel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
leading: const SizedBox(width: 40, height: 40, child: const Placeholder()),
|
||||
title: Text(channel.internalName),
|
||||
);
|
||||
}
|
64
flutter/lib/pages/channel_list/root.dart
Normal file
64
flutter/lib/pages/channel_list/root.dart
Normal file
@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
|
||||
import '../../state/user_account.dart';
|
||||
import 'channel_list_item.dart';
|
||||
|
||||
class ChannelRootPage extends StatefulWidget {
|
||||
const ChannelRootPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChannelRootPage> createState() => _ChannelRootPageState();
|
||||
}
|
||||
|
||||
class _ChannelRootPageState extends State<ChannelRootPage> {
|
||||
final PagingController<int, Channel> _pagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
late UserAccount userAcc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<UserAccount>(context, listen: false);
|
||||
|
||||
if (acc.auth == null) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = await APIClient.getChannelList(acc.auth!, ChannelSelector.all);
|
||||
|
||||
_pagingController.appendLastPage(items);
|
||||
} catch (error) {
|
||||
_pagingController.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PagedListView<int, Channel>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Channel>(
|
||||
itemBuilder: (context, item, index) => ChannelListItem(
|
||||
channel: item,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
|
||||
class MessageListPage extends StatelessWidget {
|
||||
final String title;
|
||||
import '../../models/message.dart';
|
||||
import '../../state/user_account.dart';
|
||||
import 'message_list_item.dart';
|
||||
|
||||
const MessageListPage({super.key, required this.title});
|
||||
class MessageListPage extends StatefulWidget {
|
||||
const MessageListPage({super.key});
|
||||
|
||||
@override
|
||||
State<MessageListPage> createState() => _MessageListPageState();
|
||||
}
|
||||
|
||||
class _MessageListPageState extends State<MessageListPage> {
|
||||
static const _pageSize = 20; //TODO
|
||||
|
||||
final PagingController<String, Message> _pagingController = PagingController(firstPageKey: '@start');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(String thisPageToken) async {
|
||||
final acc = Provider.of<UserAccount>(context, listen: false);
|
||||
|
||||
if (acc.auth == null) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final [npt, newItems] = await APIClient.getMessageList(acc.auth!, thisPageToken, _pageSize);
|
||||
|
||||
if (npt == '@end') {
|
||||
_pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
_pagingController.appendPage(newItems, npt);
|
||||
}
|
||||
} catch (error) {
|
||||
_pagingController.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
return PagedListView<String, Message>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||
itemBuilder: (context, item, index) => MessageListItem(
|
||||
message: item,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _createChannel() {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
17
flutter/lib/pages/message_list/message_list_item.dart
Normal file
17
flutter/lib/pages/message_list/message_list_item.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
|
||||
class MessageListItem extends StatelessWidget {
|
||||
const MessageListItem({
|
||||
required this.message,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListTile(
|
||||
leading: const SizedBox(width: 40, height: 40, child: const Placeholder()),
|
||||
title: Text(message.messageID),
|
||||
);
|
||||
}
|
17
flutter/lib/pages/settings/root.dart
Normal file
17
flutter/lib/pages/settings/root.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsRootPage extends StatefulWidget {
|
||||
const SettingsRootPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||
}
|
||||
|
||||
class _SettingsRootPageState extends State<SettingsRootPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text('Settings'),
|
||||
);
|
||||
}
|
||||
}
|
@ -82,10 +82,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.1"
|
||||
flutter_staggered_grid_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_staggered_grid_view
|
||||
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -119,14 +127,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
infinite_scroll_pagination:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: infinite_scroll_pagination
|
||||
sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -292,6 +308,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
sliver_tools:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sliver_tools
|
||||
sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.12"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -41,6 +41,7 @@ dependencies:
|
||||
shared_preferences: ^2.2.2
|
||||
qr_flutter: ^4.1.0
|
||||
url_launcher: ^6.2.4
|
||||
infinite_scroll_pagination: ^4.0.0
|
||||
|
||||
|
||||
dependency_overrides:
|
||||
@ -57,7 +58,7 @@ dev_dependencies:
|
||||
# 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: ^2.0.0
|
||||
flutter_lints: ^3.0.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
Loading…
Reference in New Issue
Block a user