diff --git a/flutter/Makefile b/flutter/Makefile new file mode 100644 index 0000000..de96c3d --- /dev/null +++ b/flutter/Makefile @@ -0,0 +1,10 @@ + + +run: + dart run build_runner build + flutter run + + +gen: + dart run build_runner build + \ No newline at end of file diff --git a/flutter/README.md b/flutter/README.md index bd17bac..1594884 100644 --- a/flutter/README.md +++ b/flutter/README.md @@ -11,7 +11,7 @@ - https://pub.dev/packages/sqflite - https://pub.dev/packages/sqflite_common_ffi - + - https://pub.dev/packages/hive \ No newline at end of file diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index fdc3cbe..47ea5da 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -1,8 +1,13 @@ import 'dart:convert'; +import 'package:fl_toast/fl_toast.dart'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:simplecloudnotifier/models/api_error.dart'; import 'package:simplecloudnotifier/models/key_token_auth.dart'; import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; +import 'package:simplecloudnotifier/state/request_log.dart'; import '../models/channel.dart'; import '../models/message.dart'; @@ -21,69 +26,141 @@ enum ChannelSelector { class APIClient { static const String _base = 'https://simplecloudnotifier.de/api/v2'; - static Future verifyToken(String uid, String tok) async { - final uri = Uri.parse('$_base/users/$uid'); - final response = await http.get(uri, headers: {'Authorization': 'SCN $tok'}); + static Future _request({ + required String name, + required String method, + required String relURL, + Map? query, + required T Function(Map json)? fn, + dynamic jsonBody, + KeyTokenAuth? auth, + Map? header, + }) async { + final t0 = DateTime.now(); - return (response.statusCode == 200); - } + final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {}); - static Future getUser(String uid, String tok) async { - final uri = Uri.parse('$_base/users/$uid'); - final response = await http.get(uri, headers: {'Authorization': 'SCN $tok'}); + final req = http.Request(method, uri); - if (response.statusCode != 200) { - throw Exception('API request failed'); + if (jsonBody != null) { + req.body = jsonEncode(jsonBody); + req.headers['Content-Type'] = 'application/json'; } - return User.fromJson(jsonDecode(response.body)); + if (auth != null) { + req.headers['Authorization'] = 'SCN ${auth.token}'; + } + + req.headers['User-Agent'] = 'simplecloudnotifier/flutter/${Globals().platform.replaceAll(' ', '_')} ${Globals().version}+${Globals().buildNumber}'; + + if (header != null && !header.isEmpty) { + req.headers.addAll(header); + } + + int responseStatusCode = 0; + String responseBody = ''; + Map responseHeaders = {}; + + try { + final response = await req.send(); + responseBody = await response.stream.bytesToString(); + responseStatusCode = response.statusCode; + responseHeaders = response.headers; + } catch (exc, trace) { + RequestLog.addRequestException(name, t0, method, uri, req.body, req.headers, exc, trace); + showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + rethrow; + } + + if (responseStatusCode != 200) { + try { + final apierr = APIError.fromJson(jsonDecode(responseBody)); + + RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); + showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + throw Exception(apierr.message); + } catch (_) {} + + RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); + showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + throw Exception('API request failed with status code ${responseStatusCode}'); + } + + try { + final data = jsonDecode(responseBody); + + if (fn != null) { + final result = fn(data); + RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); + return result; + } else { + RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); + return null as T; + } + } catch (exc, trace) { + RequestLog.addRequestDecodeError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, exc, trace); + showPlatformToast(child: Text('Request "${name}" is fehlgeschlagen'), context: ToastProvider.context); + rethrow; + } + } + + // ========================================================================================================================================================== + + static Future verifyToken(String uid, String tok) async { + try { + await _request( + name: 'verifyToken', + method: 'GET', + relURL: '/users/$uid', + fn: null, + auth: KeyTokenAuth(userId: uid, token: tok), + ); + return true; + } catch (e) { + return false; + } + } + + static Future getUser(KeyTokenAuth auth, String uid) async { + return await _request( + name: 'getUser', + method: 'GET', + relURL: 'users/$uid', + fn: User.fromJson, + auth: auth, + ); } static Future> 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((e) => ChannelWithSubscription.fromJson(e)).toList() as List; + return await _request( + name: 'getChannelList', + method: 'GET', + relURL: 'users/${auth.userId}/channels', + query: {'selector': sel.apiKey}, + fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels']), + auth: auth, + ); } static Future<(String, List)> 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((e) => Message.fromJson(e)).toList() as List; - - return (npt, messages); + return await _request( + name: 'getMessageList', + method: 'GET', + relURL: 'messages', + query: {'next_page_token': pageToken, if (pageSize != null) 'page_size': pageSize.toString()}, + fn: (json) => Message.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), + auth: auth, + ); } static Future getMessage(KeyTokenAuth auth, String msgid) async { - final uri = Uri.parse('$_base/messages/$msgid'); - final response = await http.get(uri, headers: {'Authorization': 'SCN ${auth.token}'}); - - if (response.statusCode != 200) { - throw Exception('API request failed'); - } - - return Message.fromJson(jsonDecode(response.body)); + return await _request( + name: 'getMessage', + method: 'GET', + relURL: 'messages/$msgid', + query: {}, + fn: Message.fromJson, + auth: auth, + ); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6efde5a..4b05bef 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,13 +1,34 @@ +import 'package:fl_toast/fl_toast.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:simplecloudnotifier/state/database.dart'; +import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; +import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/user_account.dart'; void main() async { - await SCNDatabase.create(); + WidgetsFlutterBinding.ensureInitialized(); + + await Hive.initFlutter(); + await Globals().init(); + + Hive.registerAdapter(SCNRequestAdapter()); + Hive.registerAdapter(SCNLogAdapter()); + + try { + await Hive.openBox('scn-requests'); + await Hive.openBox('scn-logs'); + } catch (e) { + print(e); + Hive.deleteBoxFromDisk('scn-requests'); + Hive.deleteBoxFromDisk('scn-logs'); + await Hive.openBox('scn-requests'); + await Hive.openBox('scn-logs'); + } runApp( MultiProvider( @@ -31,7 +52,7 @@ class SCNApp extends StatelessWidget { @override Widget build(BuildContext context) { - Provider.of(context); // ensure UserAccount is loaded + Provider.of(context); // ensure UserAccount is loaded (unneccessary if lazy: false is set in MultiProvider ??) return Consumer( builder: (context, appTheme, child) => MaterialApp( @@ -40,7 +61,7 @@ class SCNApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light), useMaterial3: true, ), - home: const SCNNavLayout(), + home: const ToastProvider(child: SCNNavLayout()), ), ); } diff --git a/flutter/lib/models/api_error.dart b/flutter/lib/models/api_error.dart new file mode 100644 index 0000000..374d712 --- /dev/null +++ b/flutter/lib/models/api_error.dart @@ -0,0 +1,22 @@ +class APIError { + final String success; + final String error; + final String errhighlight; + final String message; + + const APIError({ + required this.success, + required this.error, + required this.errhighlight, + required this.message, + }); + + factory APIError.fromJson(Map json) { + return APIError( + success: json['success'], + error: json['error'], + errhighlight: json['errhighlight'], + message: json['message'], + ); + } +} diff --git a/flutter/lib/models/channel.dart b/flutter/lib/models/channel.dart index 9b2789f..e92dad3 100644 --- a/flutter/lib/models/channel.dart +++ b/flutter/lib/models/channel.dart @@ -24,31 +24,17 @@ class Channel { }); factory Channel.fromJson(Map 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.'), - }; + return Channel( + channelID: json['channel_id'], + ownerUserID: json['owner_user_id'], + internalName: json['internal_name'], + displayName: json['display_name'], + descriptionName: json['description_name'], + subscribeKey: json['subscribe_key'], + timestampCreated: json['timestamp_created'], + timestampLastSent: json['timestamp_lastsent'], + messagesSent: json['messages_sent'], + ); } } @@ -69,32 +55,21 @@ class ChannelWithSubscription extends Channel { }); factory ChannelWithSubscription.fromJson(Map 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.'), - }; + return ChannelWithSubscription( + channelID: json['channel_id'], + ownerUserID: json['owner_user_id'], + internalName: json['internal_name'], + displayName: json['display_name'], + descriptionName: json['description_name'], + subscribeKey: json['subscribe_key'], + timestampCreated: json['timestamp_created'], + timestampLastSent: json['timestamp_lastsent'], + messagesSent: json['messages_sent'], + subscription: Subscription.fromJson(json['subscription']), + ); + } + + static List fromJsonArray(List jsonArr) { + return jsonArr.map((e) => ChannelWithSubscription.fromJson(e)).toList(); } } diff --git a/flutter/lib/models/message.dart b/flutter/lib/models/message.dart index b61b2b9..faa65fd 100644 --- a/flutter/lib/models/message.dart +++ b/flutter/lib/models/message.dart @@ -30,38 +30,28 @@ class Message { }); factory Message.fromJson(Map 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.'), - }; + return Message( + messageID: json['message_id'], + senderUserID: json['sender_user_id'], + channelInternalName: json['channel_internal_name'], + channelID: json['channel_id'], + senderName: json['sender_name'], + senderIP: json['sender_ip'], + timestamp: json['timestamp'], + title: json['title'], + content: json['content'], + priority: json['priority'], + userMessageID: json['usr_message_id'], + usedKeyID: json['used_key_id'], + trimmed: json['trimmed'], + ); + } + + static fromPaginatedJsonArray(Map data, String keyMessages, String keyToken) { + final npt = data[keyToken] as String; + + final messages = (data[keyMessages] as List).map((e) => Message.fromJson(e)).toList(); + + return (npt, messages); } } diff --git a/flutter/lib/models/subscription.dart b/flutter/lib/models/subscription.dart index 0ad07cb..b8432f4 100644 --- a/flutter/lib/models/subscription.dart +++ b/flutter/lib/models/subscription.dart @@ -18,26 +18,14 @@ class Subscription { }); factory Subscription.fromJson(Map 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.'), - }; + return Subscription( + subscriptionID: json['subscription_id'], + subscriberUserID: json['subscriber_user_id'], + channelOwnerUserID: json['channel_owner_user_id'], + channelID: json['channel_id'], + channelInternalName: json['channel_internal_name'], + timestampCreated: json['timestamp_created'], + confirmed: json['confirmed'], + ); } } diff --git a/flutter/lib/models/user.dart b/flutter/lib/models/user.dart index 3dec1e3..28cc99a 100644 --- a/flutter/lib/models/user.dart +++ b/flutter/lib/models/user.dart @@ -40,48 +40,25 @@ class User { }); factory User.fromJson(Map json) { - return switch (json) { - { - 'user_id': String userID, - 'username': String? username, - 'timestamp_created': String timestampCreated, - 'timestamp_lastread': String? timestampLastRead, - 'timestamp_lastsent': String? timestampLastSent, - 'messages_sent': int messagesSent, - 'quota_used': int quotaUsed, - 'quota_remaining': int quotaRemaining, - 'quota_max': int quotaPerDay, - 'is_pro': bool isPro, - 'default_channel': String defaultChannel, - 'max_body_size': int maxBodySize, - 'max_title_length': int maxTitleLength, - 'default_priority': int defaultPriority, - 'max_channel_name_length': int maxChannelNameLength, - 'max_channel_description_length': int maxChannelDescriptionLength, - 'max_sender_name_length': int maxSenderNameLength, - 'max_user_message_id_length': int maxUserMessageIDLength, - } => - User( - userID: userID, - username: username, - timestampCreated: timestampCreated, - timestampLastRead: timestampLastRead, - timestampLastSent: timestampLastSent, - messagesSent: messagesSent, - quotaUsed: quotaUsed, - quotaRemaining: quotaRemaining, - quotaPerDay: quotaPerDay, - isPro: isPro, - defaultChannel: defaultChannel, - maxBodySize: maxBodySize, - maxTitleLength: maxTitleLength, - defaultPriority: defaultPriority, - maxChannelNameLength: maxChannelNameLength, - maxChannelDescriptionLength: maxChannelDescriptionLength, - maxSenderNameLength: maxSenderNameLength, - maxUserMessageIDLength: maxUserMessageIDLength, - ), - _ => throw const FormatException('Failed to decode User.'), - }; + return User( + userID: json['user_id'], + username: json['username'], + timestampCreated: json['timestamp_created'], + timestampLastRead: json['timestamp_lastread'], + timestampLastSent: json['timestamp_lastsent'], + messagesSent: json['messages_sent'], + quotaUsed: json['quota_used'], + quotaRemaining: json['quota_remaining'], + quotaPerDay: json['quota_max'], + isPro: json['is_pro'], + defaultChannel: json['default_channel'], + maxBodySize: json['max_body_size'], + maxTitleLength: json['max_title_length'], + defaultPriority: json['default_priority'], + maxChannelNameLength: json['max_channel_name_length'], + maxChannelDescriptionLength: json['max_channel_description_length'], + maxSenderNameLength: json['max_sender_name_length'], + maxUserMessageIDLength: json['max_user_message_id_length'], + ); } } diff --git a/flutter/lib/pages/debug/debug_logs.dart b/flutter/lib/pages/debug/debug_logs.dart new file mode 100644 index 0000000..e95c5ec --- /dev/null +++ b/flutter/lib/pages/debug/debug_logs.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class DebugLogsPage extends StatefulWidget { + @override + _DebugLogsPageState createState() => _DebugLogsPageState(); +} + +class _DebugLogsPageState extends State { + @override + Widget build(BuildContext context) { + return Container(/* Add your UI components here */); + } +} diff --git a/flutter/lib/pages/debug/debug_main.dart b/flutter/lib/pages/debug/debug_main.dart index 838fa35..a9320b8 100644 --- a/flutter/lib/pages/debug/debug_main.dart +++ b/flutter/lib/pages/debug/debug_main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/pages/debug/debug_colors.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_logs.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence.dart'; import 'package:simplecloudnotifier/pages/debug/debug_requests.dart'; @@ -9,13 +10,14 @@ class DebugMainPage extends StatefulWidget { _DebugMainPageState createState() => _DebugMainPageState(); } -enum DebugMainPageSubPage { colors, requests, persistence } +enum DebugMainPageSubPage { colors, requests, persistence, logs } class _DebugMainPageState extends State { final Map _subpages = { DebugMainPageSubPage.colors: DebugColorsPage(), DebugMainPageSubPage.requests: DebugRequestsPage(), DebugMainPageSubPage.persistence: DebugPersistencePage(), + DebugMainPageSubPage.logs: DebugLogsPage(), }; DebugMainPageSubPage _subPage = DebugMainPageSubPage.colors; @@ -52,6 +54,7 @@ class _DebugMainPageState extends State { ButtonSegment(value: DebugMainPageSubPage.colors, label: Text('Theme')), ButtonSegment(value: DebugMainPageSubPage.requests, label: Text('Requests')), ButtonSegment(value: DebugMainPageSubPage.persistence, label: Text('Persistence')), + ButtonSegment(value: DebugMainPageSubPage.logs, label: Text('Logs')), ], selected: {_subPage}, onSelectionChanged: (Set v) { diff --git a/flutter/lib/pages/debug/debug_request_view.dart b/flutter/lib/pages/debug/debug_request_view.dart new file mode 100644 index 0000000..9b3d7fb --- /dev/null +++ b/flutter/lib/pages/debug/debug_request_view.dart @@ -0,0 +1,87 @@ +import 'package:fl_toast/fl_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/state/request_log.dart'; + +class DebugRequestViewPage extends StatelessWidget { + final SCNRequest request; + + DebugRequestViewPage({required this.request}); + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: 'Request', + showSearch: false, + showDebug: false, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ...buildRow(context, "Name", request.name), + ...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()), + ...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()), + ...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()), + Divider(), + ...buildRow(context, "Method", request.method), + ...buildRow(context, "URL", request.url), + if (request.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')), + if (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody), + Divider(), + if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()), + if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody), + if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')), + Divider(), + if (request.error != '') ...buildRow(context, "Error", request.error), + if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace), + ], + ), + ), + ), + ); + } + + List buildRow(BuildContext context, String title, String value) { + return [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0), + child: Row( + children: [ + Expanded( + child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)), + ), + IconButton( + icon: FaIcon( + FontAwesomeIcons.copy, + ), + iconSize: 14, + padding: EdgeInsets.fromLTRB(0, 0, 4, 0), + constraints: BoxConstraints(), + onPressed: () { + Clipboard.setData(new ClipboardData(text: value)); + showPlatformToast(child: Text('Copied to clipboard'), context: ToastProvider.context); + }, + ), + ], + ), + ), + Card.filled( + shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), + color: request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0), + child: SelectableText( + value, + minLines: 1, + maxLines: 10, + ), + ), + ), + ]; + } +} diff --git a/flutter/lib/pages/debug/debug_requests.dart b/flutter/lib/pages/debug/debug_requests.dart index 96e7850..65f0754 100644 --- a/flutter/lib/pages/debug/debug_requests.dart +++ b/flutter/lib/pages/debug/debug_requests.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:simplecloudnotifier/pages/debug/debug_request_view.dart'; +import 'package:simplecloudnotifier/state/request_log.dart'; class DebugRequestsPage extends StatefulWidget { @override @@ -6,8 +10,82 @@ class DebugRequestsPage extends StatefulWidget { } class _DebugRequestsPageState extends State { + Box requestsBox = Hive.box('scn-requests'); + + static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); + @override Widget build(BuildContext context) { - return Container(/* Add your UI components here */); + return Container( + child: ValueListenableBuilder( + valueListenable: requestsBox.listenable(), + builder: (context, Box box, _) { + return ListView.builder( + itemCount: requestsBox.length, + itemBuilder: (context, listIndex) { + final req = requestsBox.getAt(requestsBox.length - listIndex - 1)!; + if (req.type == 'SUCCESS') { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), + child: GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), + child: ListTile( + title: Row( + children: [ + SizedBox( + width: 120, + child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + ), + Expanded( + child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), + ), + SizedBox(width: 2), + Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), + ], + ), + subtitle: Text(req.type), + ), + ), + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0), + child: GestureDetector( + onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugRequestViewPage(request: req))), + child: ListTile( + tileColor: Theme.of(context).colorScheme.errorContainer, + textColor: Theme.of(context).colorScheme.onErrorContainer, + title: Row( + children: [ + SizedBox( + width: 120, + child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + ), + Expanded( + child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), + ), + SizedBox(width: 2), + Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(req.type), + Text( + req.error, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + )), + ), + ); + } + }, + ); + }, + ), + ); } } diff --git a/flutter/lib/state/application_log.dart b/flutter/lib/state/application_log.dart index 8b13789..e96b75e 100644 --- a/flutter/lib/state/application_log.dart +++ b/flutter/lib/state/application_log.dart @@ -1 +1,29 @@ +import 'package:hive_flutter/hive_flutter.dart'; +part 'application_log.g.dart'; + +class ApplicationLog {} + +enum SCNLogLevel { debug, info, warning, error, fatal } + +@HiveType(typeId: 101) +class SCNLog extends HiveObject { + @HiveField(0) + final DateTime timestamp; + @HiveField(1) + final SCNLogLevel level; + @HiveField(2) + final String message; + @HiveField(3) + final String additional; + @HiveField(4) + final String trace; + + SCNLog( + this.timestamp, + this.level, + this.message, + this.additional, + this.trace, + ); +} diff --git a/flutter/lib/state/application_log.g.dart b/flutter/lib/state/application_log.g.dart new file mode 100644 index 0000000..36426b0 --- /dev/null +++ b/flutter/lib/state/application_log.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'application_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SCNLogAdapter extends TypeAdapter { + @override + final int typeId = 101; + + @override + SCNLog read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SCNLog( + fields[0] as DateTime, + fields[1] as SCNLogLevel, + fields[2] as String, + fields[3] as String, + fields[4] as String, + ); + } + + @override + void write(BinaryWriter writer, SCNLog obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.timestamp) + ..writeByte(1) + ..write(obj.level) + ..writeByte(2) + ..write(obj.message) + ..writeByte(3) + ..write(obj.additional) + ..writeByte(4) + ..write(obj.trace); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SCNLogAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutter/lib/state/database.dart b/flutter/lib/state/database.dart deleted file mode 100644 index 547f88b..0000000 --- a/flutter/lib/state/database.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:path_provider/path_provider.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import 'dart:io'; -import 'package:path/path.dart' as path; - -class SCNDatabase { - static SCNDatabase? instance = null; - - final Database _db; - - SCNDatabase._(this._db) {} - - static create() async { - var docPath = await getApplicationDocumentsDirectory(); - var dbpath = path.join(docPath.absolute.path, 'scn.db'); - - if (Platform.isWindows || Platform.isLinux) { - sqfliteFfiInit(); - } - - var db = await databaseFactoryFfi.openDatabase(dbpath, - options: OpenDatabaseOptions( - version: 1, - onCreate: (db, version) async { - initDatabase(db); - }, - onUpgrade: (db, oldVersion, newVersion) async { - upgradeDatabase(db, oldVersion, newVersion); - }, - )); - - return instance = SCNDatabase._(db); - } - - static void initDatabase(Database db) async { - await db.execute('CREATE TABLE requests (id INTEGER PRIMARY KEY, timestamp DATETIME, name TEXT, url TEXT, response_code INTEGER, response TEXT, status TEXT)'); - - await db.execute('CREATE TABLE logs (id INTEGER PRIMARY KEY, timestamp DATETIME, level TEXT, text TEXT, additional TEXT)'); - - await db.execute('CREATE TABLE messages (message_id INTEGER PRIMARY KEY, receive_timestamp DATETIME, channel_id TEXT, timestamp TEXT, data JSON)'); - } - - static void upgradeDatabase(Database db, int oldVersion, int newVersion) { - // ... - } -} diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart new file mode 100644 index 0000000..70287aa --- /dev/null +++ b/flutter/lib/state/globals.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:package_info_plus/package_info_plus.dart'; + +class Globals { + static final Globals _singleton = Globals._internal(); + + factory Globals() { + return _singleton; + } + + Globals._internal(); + + String appName = ''; + String packageName = ''; + String version = ''; + String buildNumber = ''; + String platform = ''; + String hostname = ''; + + init() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + this.appName = packageInfo.appName; + this.packageName = packageInfo.packageName; + this.version = packageInfo.version; + this.buildNumber = packageInfo.buildNumber; + this.platform = Platform.operatingSystem; + this.hostname = Platform.localHostname; + } +} diff --git a/flutter/lib/state/request_log.dart b/flutter/lib/state/request_log.dart index 8b13789..1014217 100644 --- a/flutter/lib/state/request_log.dart +++ b/flutter/lib/state/request_log.dart @@ -1 +1,144 @@ +import 'package:hive/hive.dart'; +import 'package:simplecloudnotifier/models/api_error.dart'; +part 'request_log.g.dart'; + +class RequestLog { + static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map reqheaders, dynamic e, StackTrace trace) { + Hive.box('scn-requests').add(SCNRequest( + timestampStart: tStart, + timestampEnd: DateTime.now(), + name: name, + method: method, + url: uri.toString(), + requestHeaders: reqheaders, + requestBody: reqbody, + responseStatusCode: 0, + responseHeaders: {}, + responseBody: '', + type: 'EXCEPTION', + error: (e is Exception) ? e.toString() : '$e', + stackTrace: trace.toString(), + )); + } + + static void addRequestAPIError(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders, APIError apierr) { + Hive.box('scn-requests').add(SCNRequest( + timestampStart: t0, + timestampEnd: DateTime.now(), + name: name, + method: method, + url: uri.toString(), + requestHeaders: reqheaders, + requestBody: reqbody, + responseStatusCode: responseStatusCode, + responseHeaders: responseHeaders, + responseBody: responseBody, + type: 'API_ERROR', + error: apierr.message, + stackTrace: '', + )); + } + + static void addRequestErrorStatuscode(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders) { + Hive.box('scn-requests').add(SCNRequest( + timestampStart: t0, + timestampEnd: DateTime.now(), + name: name, + method: method, + url: uri.toString(), + requestHeaders: reqheaders, + requestBody: reqbody, + responseStatusCode: responseStatusCode, + responseHeaders: responseHeaders, + responseBody: responseBody, + type: 'ERROR_STATUSCODE', + error: 'API request failed with status code $responseStatusCode', + stackTrace: '', + )); + } + + static void addRequestSuccess(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders) { + Hive.box('scn-requests').add(SCNRequest( + timestampStart: t0, + timestampEnd: DateTime.now(), + name: name, + method: method, + url: uri.toString(), + requestHeaders: reqheaders, + requestBody: reqbody, + responseStatusCode: responseStatusCode, + responseHeaders: responseHeaders, + responseBody: responseBody, + type: 'SUCCESS', + error: '', + stackTrace: '', + )); + } + + static void addRequestDecodeError(String name, DateTime t0, String method, Uri uri, String reqbody, Map reqheaders, int responseStatusCode, String responseBody, Map responseHeaders, Object exc, StackTrace trace) { + Hive.box('scn-requests').add(SCNRequest( + timestampStart: t0, + timestampEnd: DateTime.now(), + name: name, + method: method, + url: uri.toString(), + requestHeaders: reqheaders, + requestBody: reqbody, + responseStatusCode: responseStatusCode, + responseHeaders: responseHeaders, + responseBody: responseBody, + type: 'DECODE_ERROR', + error: (exc is Exception) ? exc.toString() : '$exc', + stackTrace: trace.toString(), + )); + } +} + +@HiveType(typeId: 100) +class SCNRequest extends HiveObject { + @HiveField(0) + final DateTime timestampStart; + @HiveField(1) + final DateTime timestampEnd; + @HiveField(2) + final String name; + @HiveField(3) + final String type; + @HiveField(4) + final String error; + @HiveField(5) + final String stackTrace; + + @HiveField(6) + final String method; + @HiveField(7) + final String url; + @HiveField(8) + final Map requestHeaders; + @HiveField(12) + final String requestBody; + + @HiveField(9) + final int responseStatusCode; + @HiveField(10) + final Map responseHeaders; + @HiveField(11) + final String responseBody; + + SCNRequest({ + required this.timestampStart, + required this.timestampEnd, + required this.name, + required this.method, + required this.url, + required this.requestHeaders, + required this.requestBody, + required this.responseStatusCode, + required this.responseHeaders, + required this.responseBody, + required this.type, + required this.error, + required this.stackTrace, + }); +} diff --git a/flutter/lib/state/request_log.g.dart b/flutter/lib/state/request_log.g.dart new file mode 100644 index 0000000..c2007c6 --- /dev/null +++ b/flutter/lib/state/request_log.g.dart @@ -0,0 +1,77 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'request_log.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SCNRequestAdapter extends TypeAdapter { + @override + final int typeId = 100; + + @override + SCNRequest read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SCNRequest( + timestampStart: fields[0] as DateTime, + timestampEnd: fields[1] as DateTime, + name: fields[2] as String, + method: fields[6] as String, + url: fields[7] as String, + requestHeaders: (fields[8] as Map).cast(), + requestBody: fields[12] as String, + responseStatusCode: fields[9] as int, + responseHeaders: (fields[10] as Map).cast(), + responseBody: fields[11] as String, + type: fields[3] as String, + error: fields[4] as String, + stackTrace: fields[5] as String, + ); + } + + @override + void write(BinaryWriter writer, SCNRequest obj) { + writer + ..writeByte(13) + ..writeByte(0) + ..write(obj.timestampStart) + ..writeByte(1) + ..write(obj.timestampEnd) + ..writeByte(2) + ..write(obj.name) + ..writeByte(3) + ..write(obj.type) + ..writeByte(4) + ..write(obj.error) + ..writeByte(5) + ..write(obj.stackTrace) + ..writeByte(6) + ..write(obj.method) + ..writeByte(7) + ..write(obj.url) + ..writeByte(8) + ..write(obj.requestHeaders) + ..writeByte(12) + ..write(obj.requestBody) + ..writeByte(9) + ..write(obj.responseStatusCode) + ..writeByte(10) + ..write(obj.responseHeaders) + ..writeByte(11) + ..write(obj.responseBody); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SCNRequestAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutter/lib/state/user_account.dart b/flutter/lib/state/user_account.dart index 12de32a..d2fb8b8 100644 --- a/flutter/lib/state/user_account.dart +++ b/flutter/lib/state/user_account.dart @@ -71,7 +71,7 @@ class UserAccount extends ChangeNotifier { throw Exception('Not authenticated'); } - final user = await APIClient.getUser(_auth!.userId, _auth!.token); + final user = await APIClient.getUser(_auth!, _auth!.userId); setUser(user); diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift index b19945c..9506405 100644 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import package_info_plus import path_provider_foundation import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 059c06f..254cac0 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -17,6 +41,70 @@ packages: url: "https://pub.dev" source: hosted 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: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + 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: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + 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" characters: dependency: transitive description: @@ -25,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -33,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -41,6 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -49,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" fake_async: dependency: transitive description: @@ -73,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + fl_toast: + dependency: "direct main" + description: + name: fl_toast + sha256: "0f7bbce90d1b75463a414c6a5476e45bd93fa7c4adccce1690076f4d1ef77c42" + url: "https://pub.dev" + source: hosted + version: "3.2.0" flutter: dependency: "direct main" description: flutter @@ -111,6 +255,54 @@ packages: relative: true source: path 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" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" http: dependency: "direct main" description: @@ -119,6 +311,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -143,6 +343,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -175,6 +399,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: @@ -199,6 +431,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" nested: dependency: transitive description: @@ -207,6 +447,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.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: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: transitive description: @@ -279,6 +543,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" provider: dependency: "direct main" description: @@ -287,6 +559,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" qr: dependency: transitive description: @@ -359,6 +647,22 @@ packages: url: "https://pub.dev" source: hosted 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: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -372,6 +676,22 @@ packages: url: "https://pub.dev" source: hosted 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_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" source_span: dependency: transitive description: @@ -380,30 +700,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - sqflite_common_ffi: - dependency: "direct main" - description: - name: sqflite_common_ffi - sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" - url: "https://pub.dev" - source: hosted - version: "2.3.3" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 - url: "https://pub.dev" - source: hosted - version: "2.4.3" stack_trace: dependency: transitive description: @@ -420,6 +716,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: @@ -428,14 +732,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -452,6 +748,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" typed_data: dependency: transitive description: @@ -540,6 +844,14 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: @@ -548,6 +860,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" win32: dependency: transitive description: @@ -564,6 +884,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.19.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 16ccbf1..9a98857 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -1,40 +1,16 @@ name: simplecloudnotifier description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# 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 +publish_to: 'none' -# 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: 2.0.0+100 environment: sdk: '>=3.2.6 <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: flutter: sdk: flutter font_awesome_flutter: '>= 4.7.0' - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 http: ^1.2.0 provider: ^6.1.1 @@ -43,8 +19,11 @@ dependencies: url_launcher: ^6.2.4 infinite_scroll_pagination: ^4.0.0 intl: ^0.19.0 - sqflite_common_ffi: ^2.3.3 path_provider: ^2.1.3 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + package_info_plus: ^8.0.0 + fl_toast: ^3.2.0 dependency_overrides: @@ -56,51 +35,9 @@ dev_dependencies: flutter_test: 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 + hive_generator: ^2.0.1 + build_runner: ^2.1.4 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. 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 - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # 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