diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 8d187ee6..92d81353 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -7,27 +7,24 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.VERSION }} - deploy: ${{ steps.version.outputs.SHOULD_DEPLOY }} steps: - uses: actions/checkout@v2 with: fetch-depth: '0' - id: bump_version - if: ${{ !github.event.issue.pull_request && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') }} + if: ${{ github.ref == 'refs/heads/master' || (!github.event.issue.pull_request && github.ref == 'refs/heads/develop') }} uses: anothrNick/github-tag-action@1.26.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_BRANCHES: .* + RELEASE_BRANCHES: master DRY_RUN: true - id: version name: Create Version run: | if [ -n "${{ steps.bump_version.outputs.new_tag }}" ] then - echo ::set-output name=SHOULD_DEPLOY::true echo ::set-output name=VERSION::${{ steps.bump_version.outputs.new_tag }} else - echo ::set-output name=SHOULD_DEPLOY::false echo ::set-output name=VERSION::ci-$GITHUB_RUN_ID fi @@ -84,12 +81,11 @@ jobs: with: name: notifi-dmg path: dmg/ - retention-days: 1 if-no-files-found: error deploy: name: "Deploy" - if: ${{ needs.version.outputs.deploy == true }} + if: ${{ github.ref == 'refs/heads/master' || (!github.event.issue.pull_request && github.ref == 'refs/heads/develop') }} runs-on: macos-latest needs: [ checks, build, version ] steps: diff --git a/.gitignore b/.gitignore index 78e2cd7a..cdc74f31 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ app.*.symbols # Obfuscation related app.*.map.json /test/failures/ +/flog.db diff --git a/analysis_options.yaml b/analysis_options.yaml index 8768628d..4f1cbd41 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,20 +3,21 @@ analyzer: linter: rules: always_put_required_named_parameters_first: true - avoid_classes_with_only_static_members: true - sort_constructors_first: true - prefer_single_quotes: true - prefer_double_quotes: false - public_member_api_docs: false always_specify_types: true - lines_longer_than_80_chars: true + always_use_package_imports: true + avoid_classes_with_only_static_members: true + avoid_print: true + avoid_redundant_argument_values: true avoid_relative_lib_imports: true avoid_slow_async_io: true close_sinks: true + lines_longer_than_80_chars: true literal_only_boolean_expressions: true - prefer_void_to_null: true no_logic_in_create_state: true - avoid_redundant_argument_values: true - avoid_print: false + prefer_double_quotes: false prefer_foreach: true - prefer_is_empty: true \ No newline at end of file + prefer_is_empty: true + prefer_single_quotes: true + prefer_void_to_null: true + public_member_api_docs: false + sort_constructors_first: true \ No newline at end of file diff --git a/lib/local_notifications.dart b/lib/local_notifications.dart index 2795421d..2d4ea154 100644 --- a/lib/local_notifications.dart +++ b/lib/local_notifications.dart @@ -1,6 +1,5 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - -import 'notifications/notification.dart'; +import 'package:notifi/notifications/notification.dart'; Future initPushNotifications() async { final FlutterLocalNotificationsPlugin localNotifications = diff --git a/lib/notifications/db_provider.dart b/lib/notifications/db_provider.dart index 68ffdbb0..cb0e64ba 100644 --- a/lib/notifications/db_provider.dart +++ b/lib/notifications/db_provider.dart @@ -1,12 +1,11 @@ import 'dart:async'; +import 'package:notifi/notifications/notification.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; // import 'package:sqlite3/sqlite3.dart'; -import 'notification.dart'; - class DBProvider { DBProvider(this.dbPath); diff --git a/lib/notifications/notifis.dart b/lib/notifications/notifis.dart index b8723343..aed54767 100644 --- a/lib/notifications/notifis.dart +++ b/lib/notifications/notifis.dart @@ -46,7 +46,7 @@ class Notifications extends ChangeNotifier { try { id = await db.store(notification); } catch (e) { - print('Problem storing notification in db: $e'); + L.e('Problem storing notification in db: $e'); return -1; } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index dcd4c7d9..ac523764 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -4,6 +4,7 @@ import 'package:notifi/notifications/notifications_table.dart'; import 'package:notifi/notifications/notifis.dart'; import 'package:notifi/pallete.dart'; import 'package:notifi/user.dart'; +import 'package:notifi/utils.dart'; import 'package:provider/provider.dart'; class HomeScreen extends StatelessWidget { @@ -93,7 +94,11 @@ class HomeScreen extends StatelessWidget { Provider.of(context, listen: false).readAll(); } else if (index == 1) { // DELETE ALL EVENT - _deleteAllNotificationsDialogue(context); + showAlert(context, 'Delete All', + 'All notifications will be irretrievable', onOkPressed: () { + Provider.of(context, listen: false).deleteAll(); + Navigator.pop(context); + }); } }, // ignore: prefer_const_literals_to_create_immutables @@ -116,39 +121,6 @@ class HomeScreen extends StatelessWidget { )); } - Future _deleteAllNotificationsDialogue(BuildContext context) async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Delete All'), - content: const Text('All notifications will be irretrievable'), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text( - 'Cancel', - style: TextStyle(color: MyColour.grey), - ), - ), - TextButton( - onPressed: () { - Provider.of(context, listen: false) - .deleteAll(); - Navigator.pop(context); - }, - child: const Text( - 'Ok', - style: TextStyle(color: MyColour.black), - )), - ], - ); - }, - ); - } - // var waitErr; // setError(bool err) { // this.waitErr = err; diff --git a/lib/screens/logs.dart b/lib/screens/logs.dart new file mode 100644 index 00000000..6b3cf6f4 --- /dev/null +++ b/lib/screens/logs.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notifi/utils.dart'; + +class LogsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 80, + title: const Text('Logs'), + ), + body: Container( + padding: const EdgeInsets.only(left: 10.0, right: 10.0), + child: FutureBuilder( + future: L.logListView(), + // ignore: always_specify_types + builder: (BuildContext context, AsyncSnapshot f) { + if (f.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + return f.data; + }), + )); + } +} diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index bc4c0be4..54a1f201 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -9,6 +9,7 @@ import 'package:http/http.dart' as http; import 'package:launch_at_login/launch_at_login.dart'; import 'package:notifi/notifications/notifications_table.dart'; import 'package:notifi/pallete.dart'; +import 'package:notifi/screens/logs.dart'; import 'package:notifi/user.dart'; import 'package:notifi/utils.dart'; import 'package:package_info/package_info.dart'; @@ -53,7 +54,7 @@ class SettingsScreenState extends State { if (value.statusCode == 200) { _remoteVersion.value = value.body; } else { - print('problem getting /version'); + L.e('Problem getting /version from notifi.it'); } }); @@ -135,6 +136,13 @@ class SettingsScreenState extends State { SettingOption('About...', onTapCallback: () { launch('https://notifi.it'); }), + SettingOption('Logs...', onTapCallback: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => LogsScreen()), + ); + }), if (Platform.isMacOS) SettingOption( 'Quit notifi', @@ -237,7 +245,7 @@ class SettingsScreenState extends State { .requestNewUser(); if (!gotUser) { // TODO show error - print('Unable to fetch new user!'); + L.i('Unable to fetch new user!'); } }, child: const Text( diff --git a/lib/user.dart b/lib/user.dart index e7bc355c..16274e4e 100644 --- a/lib/user.dart +++ b/lib/user.dart @@ -6,21 +6,18 @@ import 'package:dio/dio.dart' as d; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart' as dot_env; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:notifi/local_notifications.dart'; +import 'package:notifi/notifications/notification.dart'; import 'package:notifi/notifications/notifis.dart'; import 'package:notifi/utils.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/status.dart' as status; -import 'notifications/notification.dart'; - final UserStore storage = UserStore(); const int requestNewUserCode = 551; @@ -72,11 +69,11 @@ class User with ChangeNotifier { if (alreadyHadCredentials && gotUser) { // TODO return message to user to tell them // that there credentials have been replaced - print('replaced your credentials...'); + L.e('Replaced your credentials...'); } } else { setError(hasErr: true); - print('attempting to create user again...'); + L.w('Attempting to create user again...'); await Future.delayed(const Duration(seconds: 5)); } } @@ -93,7 +90,7 @@ class User with ChangeNotifier { } final bool gotUser = await _newUserReq(data); if (gotUser && _ws != null) { - print('reconnecting to ws...'); + L.i('Reconnecting to ws...'); _ws.sink.close(status.normalClosure, 'new code!'); } notifyListeners(); @@ -105,7 +102,7 @@ class User with ChangeNotifier { //////// Future _initWSS() async { if (_ws != null) { - print('closing...'); + L.i('Closing already open WS...'); _ws.sink.close(); _ws = null; } @@ -121,7 +118,7 @@ class User with ChangeNotifier { 'Version': await getVersionFromPubSpec(), }; - print('connecting...'); + L.i('Connecting to WS...'); setError(hasErr: false); final IOWebSocketChannel ws = IOWebSocketChannel.connect(env['WS_ENDPOINT'], headers: headers, pingInterval: const Duration(seconds: 3)); @@ -136,9 +133,9 @@ class User with ChangeNotifier { // ignore: always_specify_types }, onError: (e) async { wsError = true; - print('WS error: $e'); + L.w('Problem with WS: $e'); }, onDone: () async { - print('ws connection closed'); + L.i('WS connection closed.'); if (wsError) { setError(hasErr: true); await Future.delayed(const Duration(seconds: 5)); @@ -150,7 +147,7 @@ class User with ChangeNotifier { } Future _newUserReq(Map data) async { - print('creating new user...'); + L.i('Creating new credentials...'); final d.Dio dio = d.Dio(); Response response; try { @@ -160,12 +157,12 @@ class User with ChangeNotifier { 'Sec-Key': env['SERVER_KEY'], }, contentType: d.Headers.formUrlEncodedContentType)); } catch (e) { - print('Problem fetching user code: $e'); + L.e('Problem fetching user code: $e'); return false; } if (response.statusCode != HttpStatus.ok) { - print('Problem fetching new code from server: $response'); + L.e('Problem fetching new code from server: $response'); return false; } @@ -175,7 +172,7 @@ class User with ChangeNotifier { credentialsMap = json.decode(response.data as String) as Map; } catch (e) { - print('Problem decoding new code from server: $e - ${response.data}'); + L.e('Problem decoding new code from server: $e - ${response.data}'); return false; } @@ -196,7 +193,7 @@ class User with ChangeNotifier { try { notifications = json.decode(msg as String) as List; } catch (e) { - print('ignoring un-parsable incoming message from server: $msg: $e'); + L.e('Ignoring un-parsable incoming message from server: $msg: $e'); return []; } @@ -207,7 +204,7 @@ class User with ChangeNotifier { try { jsonMessage = Map.from(notifications[i]); } catch (e) { - print('ignoring un-parsable ws message: $msg: $e'); + L.e('Ignoring un-parsable WS message: $msg: $e'); return []; } @@ -239,7 +236,7 @@ class User with ChangeNotifier { bool err; void setError({bool hasErr}) { - // wait for 1 second to make sure still error to prevent + // wait for 1 second to make sure hasErr hasn't changed to prevent // stuttering. err = hasErr; Future.delayed(const Duration(seconds: 1), () { @@ -263,10 +260,8 @@ class UserStore { String userJsonString; try { userJsonString = await storage.read(key: key); - } on MissingPluginException catch (_) { - // read json from file - final File file = await _getLinuxFile(); - userJsonString = await file.readAsString(); + } catch (e) { + L.f(e); } try { @@ -277,7 +272,7 @@ class UserStore { user.credentials = userJson['credentials']; user.flutterToken = userJson['flutterToken']; } catch (error) { - print(error); + L.f(error); return false; } return true; @@ -291,21 +286,8 @@ class UserStore { }); try { await storage.write(key: key, value: jsonData); - } on MissingPluginException catch (e) { - print('unable to store in keychain - storing in file $e'); - // write to file instead of keychain - final File file = await _getLinuxFile(); - file.writeAsString(jsonData); } catch (e) { - print('unable to store in keychain $e'); + L.f(e); } } - - Future _getLinuxFile() async { - final String dir = (await getApplicationDocumentsDirectory()).path; - final String savePath = '${'$dir/.notifi/'}$linuxFilePath'; - final File file = File(savePath); - file.create(recursive: true); - return file; - } } diff --git a/lib/utils.dart b/lib/utils.dart index 75f0f794..856a113a 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,7 +1,12 @@ import 'dart:io'; +import 'package:f_logs/f_logs.dart'; +import 'package:f_logs/model/flog/flog.dart'; +import 'package:f_logs/model/flog/log.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:notifi/pallete.dart'; import 'package:package_info/package_info.dart'; import 'package:toast/toast.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -19,7 +24,7 @@ Future invokeMacMethod(String method) async { try { await platform.invokeMethod(method); } on PlatformException catch (e) { - print("Failed to invoke method ($method): '${e.message}'."); + L.e("Failed to invoke method ($method): '${e.message}'."); } } } @@ -34,10 +39,111 @@ Future openUrl(String url) async { await launch(url); invokeMacMethod('close_window'); } else { - print("can't open: $url"); + L.w("Can't open: $url"); } } void showToast(String msg, BuildContext context, {int duration, int gravity}) { Toast.show(msg, context, duration: duration, gravity: gravity); } + +class L { + static void d(String msg) { + FLog.debug(text: msg); + } + + static void i(String msg) { + FLog.info(text: msg); + } + + static void w(String msg) { + FLog.warning(text: msg); + } + + static void e(String msg) { + FLog.error(text: msg); + } + + static void f(Exception msg) { + FLog.fatal(text: msg.toString(), exception: msg); + } + + static Future logListView() async { + final List logs = await FLog.getAllLogs(); + + final List rows = []; + for (int i = logs.length - 1; i >= logs.length - 100; i--) { + final Log log = logs[i]; + rows.add(Container( + padding: const EdgeInsets.only(top: 5.0, bottom: 2.0), + child: Row( + children: [ + Flexible( + child: RichText( + text: TextSpan(children: [ + TextSpan( + text: log.logLevel + .toString() + .replaceAll('LogLevel.', '') + .substring(0, 4), + style: const TextStyle( + color: MyColour.grey, + fontWeight: FontWeight.w600, + fontSize: 12, + fontFamily: 'Inconsolata'), + ), + TextSpan( + text: ' ~ ${log.timestamp}\n', + style: const TextStyle( + color: MyColour.grey, + fontWeight: FontWeight.w100, + fontSize: 12, + fontFamily: 'Inconsolata'), + ), + TextSpan( + text: log.text, + style: const TextStyle( + color: MyColour.black, + fontWeight: FontWeight.w700, + fontSize: 13, + fontFamily: 'Inconsolata'), + ), + ])), + ), + ], + ), + )); + } + return ListView(children: rows); + } +} + +Future showAlert(BuildContext context, String title, String description, + {int duration, int gravity, VoidCallback onOkPressed}) { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(description), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text( + 'Cancel', + style: TextStyle(color: MyColour.grey), + ), + ), + TextButton( + onPressed: onOkPressed, + child: const Text( + 'Ok', + style: TextStyle(color: MyColour.black), + )), + ], + ); + }, + ); +} diff --git a/pubspec.lock b/pubspec.lock index 879b18f7..5017e5c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -141,6 +141,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.10" + f_logs: + dependency: "direct main" + description: + name: f_logs + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" fake_async: dependency: transitive description: @@ -263,6 +270,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.19" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" js: dependency: transitive description: @@ -396,6 +410,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.0" + permission_handler: + dependency: transitive + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0+hotfix.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" petitparser: dependency: transitive description: @@ -452,6 +480,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.25.0" + sembast: + dependency: transitive + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.10+4" sky_engine: dependency: transitive description: flutter @@ -639,6 +674,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.5.1" + xxtea: + dependency: transitive + description: + name: xxtea + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0c5efcac..07c1f405 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: cached_network_image: ^2.2.0+1 cupertino_icons: ^1.0.2 dio: ^3.0.10 + f_logs: ^1.2.2 flutter: sdk: flutter flutter_dotenv: ^3.1.0 diff --git a/test/golden-asserts/screen/settings.png b/test/golden-asserts/screen/settings.png index 3358c97d..fd54d278 100644 Binary files a/test/golden-asserts/screen/settings.png and b/test/golden-asserts/screen/settings.png differ diff --git a/test/widget_test.dart b/test/widget_test.dart index 1b865fea..416b992d 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -42,6 +42,14 @@ void main() { testWidgets('Test Settings Navigation', (WidgetTester tester) async { await pumpWidget(tester, null); + const MethodChannel channel = + MethodChannel('plugins.flutter.io/path_provider'); + channel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'getApplicationDocumentsDirectory') { + return ''; + } + }); + // open settings await tester.tap(find.byIcon(Icons.settings)); await tester.pump(); @@ -54,6 +62,8 @@ void main() { await expectLater(find.byType(SettingsScreen), matchesGoldenFile('golden-asserts/screen/settings.png')); }); + + // TODO test log navigation }); group('Test Notification', () {