diff --git a/packages/neon/README.md b/packages/neon/README.md index ae207af1..bf03a67d 100644 --- a/packages/neon/README.md +++ b/packages/neon/README.md @@ -10,6 +10,7 @@ The app will be published on F-Droid and the Google Playstore later. For more screenshots see `./screenshots/`. -| ![](screenshots/login_server_selection.png) | ![](screenshots/settings_oled.png) | ![](screenshots/settings_news.png) | -|------------------------------------------------|------------------------------------|------------------------------------| -| ![](screenshots/news_articles_unread_list.png) | ![](screenshots/files_photos.png) | ![](screenshots/notes_edit.png) | +| ![](screenshots/login_server_selection.png) | ![](screenshots/home_drawer.png) | ![](screenshots/settings_oled.png) | +|---------------------------------------------|------------------------------------------------|--------------------------------------| +| ![](screenshots/files_photos.png) | ![](screenshots/news_articles_unread_list.png) | ![](screenshots/notes_note_edit.png) | +| ![](screenshots/notifications_list.png) | | | diff --git a/packages/neon/android/app/build.gradle b/packages/neon/android/app/build.gradle index f1a9762d..d1be878b 100644 --- a/packages/neon/android/app/build.gradle +++ b/packages/neon/android/app/build.gradle @@ -46,7 +46,7 @@ android { applicationId "de.provokateurin.neon" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/neon/integration_test/screenshot_test.dart b/packages/neon/integration_test/screenshot_test.dart new file mode 100644 index 00000000..b31fa817 --- /dev/null +++ b/packages/neon/integration_test/screenshot_test.dart @@ -0,0 +1,563 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/apps/files/app.dart'; +import 'package:neon/src/blocs/accounts.dart'; +import 'package:neon/src/blocs/capabilities.dart'; +import 'package:neon/src/blocs/push_notifications.dart'; +import 'package:neon/src/models/account.dart'; +import 'package:neon/src/neon.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MemorySharedPreferences implements SharedPreferences { + final _data = {}; + + @override + Future clear() async { + _data.clear(); + return true; + } + + @override + Future commit() async => true; + + @override + Future reload() async {} + + @override + Future remove(String key) async { + _data.remove(key); + return true; + } + + @override + Set getKeys() => _data.keys.toSet(); + + @override + bool containsKey(String key) => _data.keys.contains(key); + + @override + Object? get(String key) => _data[key]; + + @override + bool? getBool(String key) => _data[key] as bool?; + + @override + double? getDouble(String key) => _data[key] as double?; + + @override + int? getInt(String key) => _data[key] as int?; + + @override + String? getString(String key) => _data[key] as String?; + + @override + List? getStringList(String key) => (_data[key] as List).cast(); + + @override + Future setBool(String key, bool value) async { + _data[key] = value; + return true; + } + + @override + Future setDouble(String key, double value) async { + _data[key] = value; + return true; + } + + @override + Future setInt(String key, int value) async { + _data[key] = value; + return true; + } + + @override + Future setString(String key, String value) async { + _data[key] = value; + return true; + } + + @override + Future setStringList(String key, List value) async { + _data[key] = value; + return true; + } +} + +Future pumpAppPage( + final WidgetTester tester, + final IntegrationTestWidgetsFlutterBinding binding, { + required final Widget Function(BuildContext, Function(NextcloudTheme)) builder, + final Account? account, +}) async { + final sharedPreferences = MemorySharedPreferences(); + + final platform = await getNeonPlatform(); + final requestManager = RequestManager(); + final allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform); + + final packageInfo = await PackageInfo.fromPlatform(); + + final globalOptions = GlobalOptions( + Storage('global', sharedPreferences), + packageInfo, + ); + await globalOptions.pushNotificationsEnabled.set(false); + + final accountsBloc = AccountsBloc( + requestManager, + Storage('accounts', sharedPreferences), + sharedPreferences, + globalOptions, + packageInfo, + ); + if (account != null) { + accountsBloc.addAccount(account..setupClient(packageInfo)); + } + + final pushNotificationsBloc = PushNotificationsBloc( + accountsBloc, + sharedPreferences, + globalOptions, + null, + platform, + ); + + // ignore: close_sinks + final userThemeStream = BehaviorSubject(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + Provider( + create: (final _) => sharedPreferences, + ), + Provider( + create: (final _) => null, + ), + Provider( + create: (final _) => platform, + ), + Provider( + create: (final _) => globalOptions, + ), + Provider( + create: (final _) => requestManager, + ), + Provider( + create: (final _) => accountsBloc, + ), + Provider( + create: (final _) => pushNotificationsBloc, + ), + Provider>( + create: (final _) => allAppImplementations, + ), + Provider( + create: (final _) => packageInfo, + ), + ], + child: StreamBuilder( + stream: userThemeStream, + builder: (final context, final themeSnapshot) => StreamBuilder( + stream: globalOptions.themeMode.stream, + builder: (final context, final themeModeSnapshot) => StreamBuilder( + stream: globalOptions.themeOLEDAsDark.stream, + builder: (final context, final themeOLEDAsDarkSnapshot) => MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: getThemeFromNextcloudTheme( + themeSnapshot.data, + themeModeSnapshot.data ?? ThemeMode.system, + Brightness.light, + oledAsDark: themeOLEDAsDarkSnapshot.data ?? false, + ), + home: builder(context, userThemeStream.add), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); +} + +Future openDrawer(final WidgetTester tester) async { + await tester.tap(find.byTooltip('Open navigation menu')); + await tester.pumpAndSettle(); +} + +Future switchPage(final WidgetTester tester, final String name) async { + await openDrawer(tester); + await tester.tap(find.byKey(Key(name))); + await tester.pumpAndSettle(); +} + +Future prepareScreenshot(final WidgetTester tester, final IntegrationTestWidgetsFlutterBinding binding) async { + await binding.convertFlutterSurfaceToImage(); + await tester.pumpAndSettle(); +} + +Future main() async { + // The screenshots are pretty annoying on Android. See https://github.com/flutter/flutter/issues/92381 + + assert(Platform.isAndroid, 'Screenshots need to be taken on Android'); + + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final account = Account( + serverURL: 'http://10.0.2.2', + username: 'test', + password: 'supersafepasswordtocircumventpasswordpolicies', + ); + + setUpAll(() async { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + }); + + testWidgets('login', (final tester) async { + await pumpAppPage( + tester, + binding, + builder: (final context, final _) => const LoginPage(), + ); + await prepareScreenshot(tester, binding); + await binding.takeScreenshot('login_server_selection'); + + await tester.enterText(find.byType(TextFormField), account.serverURL); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); // Make sure the login webview is loaded + await tester.pumpAndSettle(); + await binding.takeScreenshot('login_form'); + }); + + testWidgets('home', (final tester) async { + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await openDrawer(tester); + await tester.pumpAndSettle(); + await tester.pump(); + await prepareScreenshot(tester, binding); + await binding.takeScreenshot('home_drawer'); + }); + + testWidgets('files', (final tester) async { + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await prepareScreenshot(tester, binding); + await binding.takeScreenshot('files_root'); + + // Show Photos folder + await tester.tap(find.text('Photos')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('files_photos'); + + // Show file actions + await tester.tap(find.text('Photos')); + await tester.pumpAndSettle(); + await tester.tap(find.byType(PopupMenuButton).first); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('files_actions'); + + // Show details page + await tester.tap(find.text('Details')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('files_details'); + + // Show create dialog + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('files_create'); + }); + + testWidgets('news', (final tester) async { + const wikipediaFeedURL = 'https://en.wikipedia.org/w/api.php?action=featuredfeed&feed=featured&feedformat=atom'; + const nasaFeedURL = 'https://www.nasa.gov/rss/dyn/breaking_news.rss'; + + final folder = await account.client.news.createFolder( + NewsCreateFolder( + name: 'test', + ), + ); + await account.client.news.addFeed( + NewsAddFeed( + url: nasaFeedURL, + folderId: folder!.folders.single.id, + ), + ); + + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await prepareScreenshot(tester, binding); + await switchPage(tester, 'app-news'); + + // Show folders + await tester.tap(find.byIcon(Icons.folder)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('news_folders_list'); + + // Add Wikipedia feed + await tester.tap(find.byIcon(Icons.rss_feed)); + await tester.pumpAndSettle(); + await tester.pump(); + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('news_feed_add'); + + // Finish adding Wikipedia feed + await tester.enterText(find.byType(TextFormField), wikipediaFeedURL); + await tester.pumpAndSettle(); + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + await tester.pumpAndSettle(); + await tester.pump(); + + await binding.takeScreenshot('news_feeds_list'); + + // Open feed + await tester.tap(find.text('NASA Breaking News')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('news_feed_articles_list'); + + // Show unread articles + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.newspaper)); + await tester.pumpAndSettle(); + + // Star two articles + await tester.tap(find.byIcon(Icons.star_outline).at(0)); + await tester.tap(find.byIcon(Icons.star_outline).at(1)); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + await binding.takeScreenshot('news_articles_unread_list'); + + // Show starred articles + await tester.tap(find.text('Unread')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Starred').last); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + await binding.takeScreenshot('news_articles_starred_list'); + }); + + testWidgets('notes', (final tester) async { + await account.client.notes.createNote( + NotesNote( + title: 'Wishlist', + category: 'Financial', + ), + ); + + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await prepareScreenshot(tester, binding); + await switchPage(tester, 'app-notes'); + + // Create note + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextFormField).first, 'Grocery'); + await tester.pumpAndSettle(); + await tester.tap(find.byType(TextFormField).last); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('notes_note_create'); + + // Finish creating note + await tester.enterText(find.byType(TextFormField).last, 'Financial'); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + // Star note + await tester.tap(find.byIcon(Icons.star_outline).first); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + await binding.takeScreenshot('notes_notes_list'); + + // Edit note + await tester.tap(find.text('Grocery')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField).first, '- Bread\n- Water\n- Apples'); + await tester.pumpAndSettle(); + await tester.pump(); // Needed for the text to actually show up + + await binding.takeScreenshot('notes_note_edit'); + + // Show note preview + await tester.tap(find.byIcon(Icons.visibility)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('notes_note_preview'); + + // Show categories + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(MdiIcons.tag).last); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('notes_categories_list'); + }); + + testWidgets('notifications', (final tester) async { + await account.client.notifications.sendAdminNotification( + account.username, + NotificationsAdminNotification( + shortMessage: 'Notifications demo', + longMessage: 'This is a notifications demo of the Neon app', + ), + ); + + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await prepareScreenshot(tester, binding); + await switchPage(tester, 'app-notifications'); + + await tester.pumpAndSettle(); + await tester.pump(); + + await binding.takeScreenshot('notifications_list'); + }); + + testWidgets('settings', (final tester) async { + await pumpAppPage( + tester, + binding, + account: account, + builder: (final context, final onThemeChanged) => HomePage( + account: account, + onThemeChanged: onThemeChanged, + ), + ); + await prepareScreenshot(tester, binding); + await switchPage(tester, 'settings'); + + // Open Files settings + await tester.tap(find.text('Files')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_app_files'); + + // Open News settings + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + await tester.tap(find.text('News')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_app_news'); + + // Open Notes settings + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Notes')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_app_notes'); + + // Go back to main page + await tester.tap(find.byIcon(Icons.arrow_back)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_light'); + + // Change to dark theme + await tester.tap(find.text('Automatic')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Dark').last); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_dark'); + + // Enable OLED theme + await tester.tap(find.byType(CheckboxListTile).first); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_oled'); + + // Change to back to light theme + await tester.tap(find.text('Dark')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Light').last); + await tester.pumpAndSettle(); + + // Scroll down to accounts + await tester.drag(find.byType(ListView), const Offset(0, -10000)); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_accounts'); + + // Go to account settings + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Settings').last); + await tester.pumpAndSettle(); + await tester.pump(); // Needed for the drop down button to actually show up + await tester.tap(find.text('Automatic')); + await tester.pumpAndSettle(); + + await binding.takeScreenshot('settings_account'); + }); +} diff --git a/packages/neon/lib/app.dart b/packages/neon/lib/app.dart index 7ba24c58..1b4876dd 100644 --- a/packages/neon/lib/app.dart +++ b/packages/neon/lib/app.dart @@ -106,7 +106,6 @@ class _NeonAppState extends State with WidgetsBindingObserver { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, navigatorKey: _navigatorKey, - debugShowCheckedModeBanner: false, theme: getThemeFromNextcloudTheme( _userTheme, themeModeSnapshot.data!, diff --git a/packages/neon/lib/main.dart b/packages/neon/lib/main.dart index 2a29a73d..7220b928 100644 --- a/packages/neon/lib/main.dart +++ b/packages/neon/lib/main.dart @@ -29,7 +29,9 @@ Future main() async { final sharedPreferences = await SharedPreferences.getInstance(); final platform = await getNeonPlatform(); - final requestManager = await getRequestManager(platform); + final cache = Cache(platform); + await cache.init(); + final requestManager = RequestManager(cache); final allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform); final packageInfo = await PackageInfo.fromPlatform(); diff --git a/packages/neon/lib/src/apps/files/widgets/browser_view.dart b/packages/neon/lib/src/apps/files/widgets/browser_view.dart index 5aafad08..3c98ab2d 100644 --- a/packages/neon/lib/src/apps/files/widgets/browser_view.dart +++ b/packages/neon/lib/src/apps/files/widgets/browser_view.dart @@ -319,11 +319,11 @@ class _FilesBrowserViewState extends State { ), ), trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions - ? PopupMenuButton<_FileAction>( + ? PopupMenuButton( itemBuilder: (final context) => [ if (details.isFavorite != null) ...[ PopupMenuItem( - value: _FileAction.toggleFavorite, + value: FilesFileAction.toggleFavorite, child: Text( details.isFavorite! ? AppLocalizations.of(context).filesRemoveFromFavorites @@ -332,43 +332,43 @@ class _FilesBrowserViewState extends State { ), ], PopupMenuItem( - value: _FileAction.details, + value: FilesFileAction.details, child: Text(AppLocalizations.of(context).filesDetails), ), PopupMenuItem( - value: _FileAction.rename, + value: FilesFileAction.rename, child: Text(AppLocalizations.of(context).rename), ), PopupMenuItem( - value: _FileAction.move, + value: FilesFileAction.move, child: Text(AppLocalizations.of(context).move), ), PopupMenuItem( - value: _FileAction.copy, + value: FilesFileAction.copy, child: Text(AppLocalizations.of(context).copy), ), // TODO: https://github.com/jld3103/nextcloud-neon/issues/4 if (!details.isDirectory) ...[ PopupMenuItem( - value: _FileAction.sync, + value: FilesFileAction.sync, child: Text(AppLocalizations.of(context).filesSync), ), ], PopupMenuItem( - value: _FileAction.delete, + value: FilesFileAction.delete, child: Text(AppLocalizations.of(context).delete), ), ], onSelected: (final action) async { switch (action) { - case _FileAction.toggleFavorite: + case FilesFileAction.toggleFavorite: if (details.isFavorite ?? false) { widget.filesBloc.removeFavorite(details.path); } else { widget.filesBloc.addFavorite(details.path); } break; - case _FileAction.details: + case FilesFileAction.details: await Navigator.of(context).push( MaterialPageRoute( builder: (final context) => FilesDetailsPage( @@ -378,7 +378,7 @@ class _FilesBrowserViewState extends State { ), ); break; - case _FileAction.rename: + case FilesFileAction.rename: final result = await showRenameDialog( context: context, title: details.isDirectory @@ -390,7 +390,7 @@ class _FilesBrowserViewState extends State { widget.filesBloc.rename(details.path, result); } break; - case _FileAction.move: + case FilesFileAction.move: final b = widget.filesBloc.getNewFilesBrowserBloc(); final originalPath = details.path.sublist(0, details.path.length - 1); b.setPath(originalPath); @@ -407,7 +407,7 @@ class _FilesBrowserViewState extends State { widget.filesBloc.move(details.path, result..add(details.name)); } break; - case _FileAction.copy: + case FilesFileAction.copy: final b = widget.filesBloc.getNewFilesBrowserBloc(); final originalPath = details.path.sublist(0, details.path.length - 1); b.setPath(originalPath); @@ -424,7 +424,7 @@ class _FilesBrowserViewState extends State { widget.filesBloc.copy(details.path, result..add(details.name)); } break; - case _FileAction.sync: + case FilesFileAction.sync: final sizeWarning = widget.bloc.options.downloadSizeWarning.value; if (sizeWarning != null && details.size > sizeWarning) { if (!(await showConfirmationDialog( @@ -439,7 +439,7 @@ class _FilesBrowserViewState extends State { } widget.filesBloc.syncFile(details.path); break; - case _FileAction.delete: + case FilesFileAction.delete: if (await showConfirmationDialog( context, details.isDirectory @@ -459,7 +459,7 @@ class _FilesBrowserViewState extends State { ); } -enum _FileAction { +enum FilesFileAction { toggleFavorite, details, rename, diff --git a/packages/neon/lib/src/apps/news/widgets/feeds_view.dart b/packages/neon/lib/src/apps/news/widgets/feeds_view.dart index 29b2e917..e94e8147 100644 --- a/packages/neon/lib/src/apps/news/widgets/feeds_view.dart +++ b/packages/neon/lib/src/apps/news/widgets/feeds_view.dart @@ -140,30 +140,30 @@ class NewsFeedsView extends StatelessWidget { ), ), ], - PopupMenuButton<_FeedAction>( + PopupMenuButton( itemBuilder: (final context) => [ PopupMenuItem( - value: _FeedAction.showURL, + value: NewsFeedAction.showURL, child: Text(AppLocalizations.of(context).newsShowFeedURL), ), PopupMenuItem( - value: _FeedAction.delete, + value: NewsFeedAction.delete, child: Text(AppLocalizations.of(context).delete), ), PopupMenuItem( - value: _FeedAction.rename, + value: NewsFeedAction.rename, child: Text(AppLocalizations.of(context).rename), ), if (folders.isNotEmpty) ...[ PopupMenuItem( - value: _FeedAction.move, + value: NewsFeedAction.move, child: Text(AppLocalizations.of(context).move), ), ], ], onSelected: (final action) async { switch (action) { - case _FeedAction.showURL: + case NewsFeedAction.showURL: await showDialog( context: context, builder: (final context) => NewsFeedShowURLDialog( @@ -171,7 +171,7 @@ class NewsFeedsView extends StatelessWidget { ), ); break; - case _FeedAction.delete: + case NewsFeedAction.delete: if (await showConfirmationDialog( context, AppLocalizations.of(context).newsRemoveFeedConfirm(feed.title!), @@ -179,7 +179,7 @@ class NewsFeedsView extends StatelessWidget { bloc.removeFeed(feed.id!); } break; - case _FeedAction.rename: + case NewsFeedAction.rename: final result = await showRenameDialog( context: context, title: AppLocalizations.of(context).newsRenameFeed, @@ -189,7 +189,7 @@ class NewsFeedsView extends StatelessWidget { bloc.renameFeed(feed.id!, result); } break; - case _FeedAction.move: + case NewsFeedAction.move: final result = await showDialog>( context: context, builder: (final context) => NewsMoveFeedDialog( @@ -224,7 +224,7 @@ class NewsFeedsView extends StatelessWidget { ); } -enum _FeedAction { +enum NewsFeedAction { showURL, delete, rename, diff --git a/packages/neon/lib/src/apps/news/widgets/folders_view.dart b/packages/neon/lib/src/apps/news/widgets/folders_view.dart index eecfd886..6926a642 100644 --- a/packages/neon/lib/src/apps/news/widgets/folders_view.dart +++ b/packages/neon/lib/src/apps/news/widgets/folders_view.dart @@ -131,20 +131,20 @@ class NewsFoldersView extends StatelessWidget { ], ), ), - trailing: PopupMenuButton<_FolderAction>( + trailing: PopupMenuButton( itemBuilder: (final context) => [ PopupMenuItem( - value: _FolderAction.delete, + value: NewsFolderAction.delete, child: Text(AppLocalizations.of(context).delete), ), PopupMenuItem( - value: _FolderAction.rename, + value: NewsFolderAction.rename, child: Text(AppLocalizations.of(context).rename), ), ], onSelected: (final action) async { switch (action) { - case _FolderAction.delete: + case NewsFolderAction.delete: if (await showConfirmationDialog( context, AppLocalizations.of(context).newsDeleteFolderConfirm(folderFeedsWrapper.folder.name!), @@ -152,7 +152,7 @@ class NewsFoldersView extends StatelessWidget { bloc.deleteFolder(folderFeedsWrapper.folder.id!); } break; - case _FolderAction.rename: + case NewsFolderAction.rename: final result = await showRenameDialog( context: context, title: AppLocalizations.of(context).newsRenameFolder, @@ -184,7 +184,7 @@ class NewsFoldersView extends StatelessWidget { } } -enum _FolderAction { +enum NewsFolderAction { delete, rename, } diff --git a/packages/neon/lib/src/blocs/capabilities.dart b/packages/neon/lib/src/blocs/capabilities.dart index 7cd61b9e..ecd6dd76 100644 --- a/packages/neon/lib/src/blocs/capabilities.dart +++ b/packages/neon/lib/src/blocs/capabilities.dart @@ -9,7 +9,9 @@ part 'capabilities.rxb.g.dart'; typedef Capabilities = CoreServerCapabilitiesOcsData; typedef NextcloudTheme = CoreServerCapabilitiesOcsDataCapabilitiesTheming; -abstract class CapabilitiesBlocEvents {} +abstract class CapabilitiesBlocEvents { + void refresh(); +} abstract class CapabilitiesBlocStates { BehaviorSubject> get capabilities; @@ -21,6 +23,8 @@ class CapabilitiesBloc extends $CapabilitiesBloc { this._requestManager, this._client, ) { + _$refreshEvent.listen((final _) => _loadCapabilities()); + _loadCapabilities(); } diff --git a/packages/neon/lib/src/blocs/capabilities.rxb.g.dart b/packages/neon/lib/src/blocs/capabilities.rxb.g.dart index 24e24dd1..a97a50d5 100644 --- a/packages/neon/lib/src/blocs/capabilities.rxb.g.dart +++ b/packages/neon/lib/src/blocs/capabilities.rxb.g.dart @@ -19,9 +19,15 @@ abstract class $CapabilitiesBloc extends RxBlocBase implements CapabilitiesBlocEvents, CapabilitiesBlocStates, CapabilitiesBlocType { final _compositeSubscription = CompositeSubscription(); + /// Тhe [Subject] where events sink to by calling [refresh] + final _$refreshEvent = PublishSubject(); + /// The state of [capabilities] implemented in [_mapToCapabilitiesState] late final BehaviorSubject> _capabilitiesState = _mapToCapabilitiesState(); + @override + void refresh() => _$refreshEvent.add(null); + @override BehaviorSubject> get capabilities => _capabilitiesState; @@ -35,6 +41,7 @@ abstract class $CapabilitiesBloc extends RxBlocBase @override void dispose() { + _$refreshEvent.close(); _compositeSubscription.dispose(); super.dispose(); } diff --git a/packages/neon/lib/src/blocs/push_notifications.dart b/packages/neon/lib/src/blocs/push_notifications.dart index ddb1239d..63dd51b0 100644 --- a/packages/neon/lib/src/blocs/push_notifications.dart +++ b/packages/neon/lib/src/blocs/push_notifications.dart @@ -28,9 +28,18 @@ class PushNotificationsBloc extends $PushNotificationsBloc { this._platform, ) { if (_platform.canUsePushNotifications) { - // We just use a single RSA keypair for all accounts - _keypair = PushUtils.loadRSAKeypair(_storage); - _setupUnifiedPush(); + UnifiedPush.getDistributors().then(_globalOptions.updateDistributors); + + _globalOptions.pushNotificationsEnabled.stream.listen((final enabled) async { + if (enabled != _pushNotificationsEnabled) { + _pushNotificationsEnabled = enabled; + if (enabled) { + // We just use a single RSA keypair for all accounts + _keypair = PushUtils.loadRSAKeypair(_storage); + await _setupUnifiedPush(); + } + } + }); } } @@ -81,7 +90,6 @@ class PushNotificationsBloc extends $PushNotificationsBloc { }, onMessage: PushUtils.onMessage, ); - await _globalOptions.updateDistributors(await UnifiedPush.getDistributors()); _globalOptions.pushNotificationsDistributor.stream.listen((final distributor) async { final disabled = distributor == null; @@ -109,7 +117,8 @@ class PushNotificationsBloc extends $PushNotificationsBloc { } Future _registerUnifiedPushInstances(final List accounts) async { - for (final account in accounts) { + // Notifications will only work on accounts with app password + for (final account in accounts.where((final a) => a.appPassword != null)) { await UnifiedPush.registerApp(account.client.id); } } @@ -120,7 +129,8 @@ class PushNotificationsBloc extends $PushNotificationsBloc { late final _storage = Storage('notifications', _sharedPreferences); final GlobalOptions _globalOptions; final Env? _env; - late final RSAKeypair? _keypair; + RSAKeypair? _keypair; + bool? _pushNotificationsEnabled; String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}'; diff --git a/packages/neon/lib/src/models/account.dart b/packages/neon/lib/src/models/account.dart index 3d628c96..a5981028 100644 --- a/packages/neon/lib/src/models/account.dart +++ b/packages/neon/lib/src/models/account.dart @@ -54,7 +54,7 @@ class Account { _client ??= NextcloudClient( serverURL, username: username, - password: password ?? appPassword, + password: appPassword ?? password, userAgentOverride: userAgent(packageInfo), ); } diff --git a/packages/neon/lib/src/pages/home/home.dart b/packages/neon/lib/src/pages/home/home.dart index 7bcc8200..0e5d7b60 100644 --- a/packages/neon/lib/src/pages/home/home.dart +++ b/packages/neon/lib/src/pages/home/home.dart @@ -449,7 +449,15 @@ class _HomePageState extends State with tray.TrayListener, WindowListe ), ], ] else ...[ - Container(), + ExceptionWidget( + capabilitiesError, + onRetry: () { + _capabilitiesBloc.refresh(); + }, + ), + CustomLinearProgressIndicator( + visible: capabilitiesLoading, + ), ], if (accountsSnapshot.data!.length != 1) ...[ DropdownButtonHideUnderline( @@ -499,6 +507,7 @@ class _HomePageState extends State with tray.TrayListener, WindowListe for (final appImplementation in appsData) ...[ if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[ ListTile( + key: Key('app-${appImplementation.id}'), title: Text(appImplementation.name(context)), leading: appImplementation.buildIcon(context), minLeadingWidth: 0, @@ -515,6 +524,7 @@ class _HomePageState extends State with tray.TrayListener, WindowListe ), ), ListTile( + key: const Key('settings'), title: Text(AppLocalizations.of(context).settings), leading: const Icon(Icons.settings), minLeadingWidth: 0, diff --git a/packages/neon/lib/src/pages/login/login.dart b/packages/neon/lib/src/pages/login/login.dart index 8428b3f8..f864f33b 100644 --- a/packages/neon/lib/src/pages/login/login.dart +++ b/packages/neon/lib/src/pages/login/login.dart @@ -84,6 +84,12 @@ class _LoginPageState extends State { }, }; + @override + void dispose() { + _loginBloc.dispose(); + super.dispose(); + } + @override Widget build(final BuildContext context) { final env = Provider.of(context); diff --git a/packages/neon/lib/src/pages/settings/settings.dart b/packages/neon/lib/src/pages/settings/settings.dart index 65f4c00e..fcb40bd2 100644 --- a/packages/neon/lib/src/pages/settings/settings.dart +++ b/packages/neon/lib/src/pages/settings/settings.dart @@ -168,20 +168,20 @@ class _SettingsPageState extends State { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - PopupMenuButton<_AccountAction>( + PopupMenuButton( itemBuilder: (final context) => [ PopupMenuItem( - value: _AccountAction.settings, + value: SettingsAccountAction.settings, child: Text(AppLocalizations.of(context).settings), ), PopupMenuItem( - value: _AccountAction.delete, + value: SettingsAccountAction.delete, child: Text(AppLocalizations.of(context).delete), ), ], onSelected: (final action) async { switch (action) { - case _AccountAction.settings: + case SettingsAccountAction.settings: await Navigator.of(context).push( MaterialPageRoute( builder: (final context) => AccountSpecificSettingsPage( @@ -191,7 +191,7 @@ class _SettingsPageState extends State { ), ); break; - case _AccountAction.delete: + case SettingsAccountAction.delete: if (await showConfirmationDialog( context, AppLocalizations.of(context).globalOptionsAccountsRemoveConfirm( @@ -328,7 +328,7 @@ class _SettingsPageState extends State { } } -enum _AccountAction { +enum SettingsAccountAction { settings, delete, } diff --git a/packages/neon/lib/src/utils/global_options.dart b/packages/neon/lib/src/utils/global_options.dart index 714bc5c4..75a9f221 100644 --- a/packages/neon/lib/src/utils/global_options.dart +++ b/packages/neon/lib/src/utils/global_options.dart @@ -10,8 +10,7 @@ class GlobalOptions { }); _pushNotificationsDistributorsSubject.listen((final distributors) async { - _pushNotificationsEnabledSubject.add(distributors.isNotEmpty); - await pushNotificationsEnabled.set(distributors.isNotEmpty); + _pushNotificationsEnabledEnabledSubject.add(distributors.isNotEmpty); await _setDefaultDistributor(); }); @@ -35,7 +34,7 @@ class GlobalOptions { final PackageInfo _packageInfo; final _accountsIDsSubject = BehaviorSubject>(); final _themeOLEDAsDarkEnabledSubject = BehaviorSubject(); - final _pushNotificationsEnabledSubject = BehaviorSubject(); + final _pushNotificationsEnabledEnabledSubject = BehaviorSubject(); final _pushNotificationsDistributorsSubject = BehaviorSubject>(); late final _distributorsMap = { @@ -128,7 +127,7 @@ class GlobalOptions { key: 'push-notifications-enabled', label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled, defaultValue: BehaviorSubject.seeded(true), - enabled: _pushNotificationsEnabledSubject, + enabled: _pushNotificationsEnabledEnabledSubject, ); late final pushNotificationsDistributor = SelectOption( diff --git a/packages/neon/lib/src/utils/push_utils.dart b/packages/neon/lib/src/utils/push_utils.dart index 9da066dd..c451b2fb 100644 --- a/packages/neon/lib/src/utils/push_utils.dart +++ b/packages/neon/lib/src/utils/push_utils.dart @@ -73,7 +73,9 @@ class PushUtils { final localizations = await AppLocalizations.delegate.load(Locale(parts[0], parts.length > 1 ? parts[1] : null)); final platform = await getNeonPlatform(); - final requestManager = await getRequestManager(platform); + final cache = Cache(platform); + await cache.init(); + final requestManager = RequestManager(cache); final allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform); final matchingAppImplementations = diff --git a/packages/neon/lib/src/utils/request_manager.dart b/packages/neon/lib/src/utils/request_manager.dart index be142393..889db050 100644 --- a/packages/neon/lib/src/utils/request_manager.dart +++ b/packages/neon/lib/src/utils/request_manager.dart @@ -1,15 +1,11 @@ part of '../neon.dart'; -Future getRequestManager(NeonPlatform platform) async { - final cache = Cache(platform); - await cache.init(); - return RequestManager(cache); -} - class RequestManager { - RequestManager(this._cache); + RequestManager([ + this.cache, + ]); - final Cache _cache; + final Cache? cache; final bool _enablePrinting = false; @@ -142,9 +138,9 @@ class RequestManager { _print('[Request]: $k'); - if ((preferCache || preloadCache) && await _cache.has(key)) { + if ((preferCache || preloadCache) && cache != null && await cache!.has(key)) { _print('[Cache]: $k'); - final s = unwrap(await deserialize((await _cache.get(key))!)); + final s = unwrap(await deserialize((await cache!.get(key))!)); if (preloadCache) { yield ResultCached(s, loading: true); } else { @@ -158,14 +154,14 @@ class RequestManager { final s = await serialize(response); _print('[Response]: $k'); - await _cache.set(key, s); + await cache?.set(key, s); yield Result.success(unwrap(response)); } on Exception catch (e) { - if (await _cache.has(key)) { + if (cache != null && await cache!.has(key)) { _print('[Cache]: $k'); debugPrint(e.toString()); - yield ResultCached(unwrap(await deserialize((await _cache.get(key))!)), error: e); + yield ResultCached(unwrap(await deserialize((await cache!.get(key))!)), error: e); return; } _print('[Failure]: $k'); @@ -261,7 +257,7 @@ class ResultCached implements Result { String tag; } -extension ResultData on Result { +extension ResultDataError on Result { T? get data { if (this is ResultSuccess) { return (this as ResultSuccess).data; @@ -273,6 +269,18 @@ extension ResultData on Result { return null; } + + Exception? get error { + if (this is ResultError) { + return (this as ResultError).error; + } + + if (this is ResultCached) { + return (this as ResultCached).error; + } + + return null; + } } extension ListAs on List { diff --git a/packages/neon/pubspec.lock b/packages/neon/pubspec.lock index 2e0a9556..c2c01d0a 100644 --- a/packages/neon/pubspec.lock +++ b/packages/neon/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "41.0.0" + version: "42.0.0" analyzer: dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.3.0" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.1.11" args: dependency: transitive description: @@ -42,7 +42,14 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" build: dependency: transitive description: @@ -161,7 +168,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.0.1" crypton: dependency: transitive description: @@ -190,6 +197,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.4" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" ffi: dependency: transitive description: @@ -244,6 +258,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.2" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_file_dialog: dependency: "direct main" description: @@ -290,14 +309,14 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.10+2" + version: "0.6.10+3" flutter_native_splash: dependency: "direct main" description: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.2.4" + version: "2.2.5" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -319,6 +338,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1+1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -331,6 +355,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -415,6 +444,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intersperse: dependency: "direct main" description: @@ -484,7 +518,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: @@ -738,7 +772,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.6.0" + version: "3.6.1" pool: dependency: transitive description: @@ -829,7 +863,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.4" + version: "0.27.5" screen_retriever: dependency: transitive description: @@ -995,7 +1029,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" sqflite: dependency: "direct main" description: @@ -1051,7 +1085,14 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" synchronized: dependency: transitive description: @@ -1065,7 +1106,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" timezone: dependency: transitive description: @@ -1093,7 +1141,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" unifiedpush: dependency: "direct main" description: @@ -1185,6 +1233,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.2.2" wakelock: dependency: "direct main" description: @@ -1234,6 +1289,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" webview_flutter: dependency: "direct main" description: @@ -1247,7 +1309,7 @@ packages: name: webview_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.8.14" + version: "2.9.2" webview_flutter_platform_interface: dependency: transitive description: @@ -1261,7 +1323,7 @@ packages: name: webview_flutter_wkwebview url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.9.1" win32: dependency: transitive description: diff --git a/packages/neon/pubspec.yaml b/packages/neon/pubspec.yaml index 4759677f..4da6a5ed 100644 --- a/packages/neon/pubspec.yaml +++ b/packages/neon/pubspec.yaml @@ -62,6 +62,12 @@ dependencies: dev_dependencies: build_runner: ^2.1.7 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter json_serializable: ^6.1.4 nit_picking: git: diff --git a/packages/neon/screenshots/files_actions.png b/packages/neon/screenshots/files_actions.png new file mode 100644 index 00000000..860567c3 Binary files /dev/null and b/packages/neon/screenshots/files_actions.png differ diff --git a/packages/neon/screenshots/files_create.png b/packages/neon/screenshots/files_create.png index 5994d064..a9c83462 100644 Binary files a/packages/neon/screenshots/files_create.png and b/packages/neon/screenshots/files_create.png differ diff --git a/packages/neon/screenshots/files_details.png b/packages/neon/screenshots/files_details.png index f6e2031b..33114822 100644 Binary files a/packages/neon/screenshots/files_details.png and b/packages/neon/screenshots/files_details.png differ diff --git a/packages/neon/screenshots/files_photos.png b/packages/neon/screenshots/files_photos.png index 5c632ab1..db45f05c 100644 Binary files a/packages/neon/screenshots/files_photos.png and b/packages/neon/screenshots/files_photos.png differ diff --git a/packages/neon/screenshots/files_root.png b/packages/neon/screenshots/files_root.png index 4c3f39a0..a3cfc126 100644 Binary files a/packages/neon/screenshots/files_root.png and b/packages/neon/screenshots/files_root.png differ diff --git a/packages/neon/screenshots/home_drawer.png b/packages/neon/screenshots/home_drawer.png new file mode 100644 index 00000000..b6b18186 Binary files /dev/null and b/packages/neon/screenshots/home_drawer.png differ diff --git a/packages/neon/screenshots/login.png b/packages/neon/screenshots/login.png deleted file mode 100644 index f2250172..00000000 Binary files a/packages/neon/screenshots/login.png and /dev/null differ diff --git a/packages/neon/screenshots/login_form.png b/packages/neon/screenshots/login_form.png new file mode 100644 index 00000000..082c9e7e Binary files /dev/null and b/packages/neon/screenshots/login_form.png differ diff --git a/packages/neon/screenshots/login_server_selection.png b/packages/neon/screenshots/login_server_selection.png index cbb189c3..aca151c2 100644 Binary files a/packages/neon/screenshots/login_server_selection.png and b/packages/neon/screenshots/login_server_selection.png differ diff --git a/packages/neon/screenshots/news_add_feed.png b/packages/neon/screenshots/news_add_feed.png deleted file mode 100644 index 74796955..00000000 Binary files a/packages/neon/screenshots/news_add_feed.png and /dev/null differ diff --git a/packages/neon/screenshots/news_articles_feed_list.png b/packages/neon/screenshots/news_articles_feed_list.png deleted file mode 100644 index c0968493..00000000 Binary files a/packages/neon/screenshots/news_articles_feed_list.png and /dev/null differ diff --git a/packages/neon/screenshots/news_articles_starred_list.png b/packages/neon/screenshots/news_articles_starred_list.png index 2cf3bc7d..dc4ef070 100644 Binary files a/packages/neon/screenshots/news_articles_starred_list.png and b/packages/neon/screenshots/news_articles_starred_list.png differ diff --git a/packages/neon/screenshots/news_articles_unread_list.png b/packages/neon/screenshots/news_articles_unread_list.png index 9c8363df..fab419cf 100644 Binary files a/packages/neon/screenshots/news_articles_unread_list.png and b/packages/neon/screenshots/news_articles_unread_list.png differ diff --git a/packages/neon/screenshots/news_feed_add.png b/packages/neon/screenshots/news_feed_add.png new file mode 100644 index 00000000..0c217c06 Binary files /dev/null and b/packages/neon/screenshots/news_feed_add.png differ diff --git a/packages/neon/screenshots/news_feed_articles_list.png b/packages/neon/screenshots/news_feed_articles_list.png new file mode 100644 index 00000000..ca132af5 Binary files /dev/null and b/packages/neon/screenshots/news_feed_articles_list.png differ diff --git a/packages/neon/screenshots/news_feeds_list.png b/packages/neon/screenshots/news_feeds_list.png index 617e5d3e..0bab52c1 100644 Binary files a/packages/neon/screenshots/news_feeds_list.png and b/packages/neon/screenshots/news_feeds_list.png differ diff --git a/packages/neon/screenshots/news_folders_list.png b/packages/neon/screenshots/news_folders_list.png index 555a2ce7..6ea37684 100644 Binary files a/packages/neon/screenshots/news_folders_list.png and b/packages/neon/screenshots/news_folders_list.png differ diff --git a/packages/neon/screenshots/notes_categories_list.png b/packages/neon/screenshots/notes_categories_list.png index 3b3da5b2..6c077e23 100644 Binary files a/packages/neon/screenshots/notes_categories_list.png and b/packages/neon/screenshots/notes_categories_list.png differ diff --git a/packages/neon/screenshots/notes_create.png b/packages/neon/screenshots/notes_create.png deleted file mode 100644 index 4b817002..00000000 Binary files a/packages/neon/screenshots/notes_create.png and /dev/null differ diff --git a/packages/neon/screenshots/notes_edit.png b/packages/neon/screenshots/notes_edit.png deleted file mode 100644 index 17d1d809..00000000 Binary files a/packages/neon/screenshots/notes_edit.png and /dev/null differ diff --git a/packages/neon/screenshots/notes_list.png b/packages/neon/screenshots/notes_list.png deleted file mode 100644 index 0b318fc9..00000000 Binary files a/packages/neon/screenshots/notes_list.png and /dev/null differ diff --git a/packages/neon/screenshots/notes_note_create.png b/packages/neon/screenshots/notes_note_create.png new file mode 100644 index 00000000..d7c22f01 Binary files /dev/null and b/packages/neon/screenshots/notes_note_create.png differ diff --git a/packages/neon/screenshots/notes_note_edit.png b/packages/neon/screenshots/notes_note_edit.png new file mode 100644 index 00000000..c09e41ba Binary files /dev/null and b/packages/neon/screenshots/notes_note_edit.png differ diff --git a/packages/neon/screenshots/notes_note_preview.png b/packages/neon/screenshots/notes_note_preview.png new file mode 100644 index 00000000..cf1adfa2 Binary files /dev/null and b/packages/neon/screenshots/notes_note_preview.png differ diff --git a/packages/neon/screenshots/notes_notes_list.png b/packages/neon/screenshots/notes_notes_list.png new file mode 100644 index 00000000..1d036d8b Binary files /dev/null and b/packages/neon/screenshots/notes_notes_list.png differ diff --git a/packages/neon/screenshots/notes_preview.png b/packages/neon/screenshots/notes_preview.png deleted file mode 100644 index 72dfc0f4..00000000 Binary files a/packages/neon/screenshots/notes_preview.png and /dev/null differ diff --git a/packages/neon/screenshots/notifications_list.png b/packages/neon/screenshots/notifications_list.png new file mode 100644 index 00000000..919ea55d Binary files /dev/null and b/packages/neon/screenshots/notifications_list.png differ diff --git a/packages/neon/screenshots/settings_account.png b/packages/neon/screenshots/settings_account.png index 65516e2c..063c5fd2 100644 Binary files a/packages/neon/screenshots/settings_account.png and b/packages/neon/screenshots/settings_account.png differ diff --git a/packages/neon/screenshots/settings_accounts.png b/packages/neon/screenshots/settings_accounts.png new file mode 100644 index 00000000..a8feba00 Binary files /dev/null and b/packages/neon/screenshots/settings_accounts.png differ diff --git a/packages/neon/screenshots/settings_app_files.png b/packages/neon/screenshots/settings_app_files.png new file mode 100644 index 00000000..f1a23c1b Binary files /dev/null and b/packages/neon/screenshots/settings_app_files.png differ diff --git a/packages/neon/screenshots/settings_app_news.png b/packages/neon/screenshots/settings_app_news.png new file mode 100644 index 00000000..6f89e5e2 Binary files /dev/null and b/packages/neon/screenshots/settings_app_news.png differ diff --git a/packages/neon/screenshots/settings_app_notes.png b/packages/neon/screenshots/settings_app_notes.png new file mode 100644 index 00000000..1ea027b5 Binary files /dev/null and b/packages/neon/screenshots/settings_app_notes.png differ diff --git a/packages/neon/screenshots/settings_dark.png b/packages/neon/screenshots/settings_dark.png new file mode 100644 index 00000000..88577683 Binary files /dev/null and b/packages/neon/screenshots/settings_dark.png differ diff --git a/packages/neon/screenshots/settings_files.png b/packages/neon/screenshots/settings_files.png deleted file mode 100644 index 0c9cd1f0..00000000 Binary files a/packages/neon/screenshots/settings_files.png and /dev/null differ diff --git a/packages/neon/screenshots/settings_light.png b/packages/neon/screenshots/settings_light.png index 2f143639..3e68f08e 100644 Binary files a/packages/neon/screenshots/settings_light.png and b/packages/neon/screenshots/settings_light.png differ diff --git a/packages/neon/screenshots/settings_news.png b/packages/neon/screenshots/settings_news.png deleted file mode 100644 index fcd52f3b..00000000 Binary files a/packages/neon/screenshots/settings_news.png and /dev/null differ diff --git a/packages/neon/screenshots/settings_notes.png b/packages/neon/screenshots/settings_notes.png deleted file mode 100644 index caefee20..00000000 Binary files a/packages/neon/screenshots/settings_notes.png and /dev/null differ diff --git a/packages/neon/screenshots/settings_oled.png b/packages/neon/screenshots/settings_oled.png index 2e34988a..0e7a11cf 100644 Binary files a/packages/neon/screenshots/settings_oled.png and b/packages/neon/screenshots/settings_oled.png differ diff --git a/packages/neon/test_driver/integration_test.dart b/packages/neon/test_driver/integration_test.dart new file mode 100644 index 00000000..dc6b6408 --- /dev/null +++ b/packages/neon/test_driver/integration_test.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() async { + final screenshotsDir = Directory('screenshots'); + if (screenshotsDir.existsSync()) { + screenshotsDir.deleteSync(recursive: true); + } + screenshotsDir.createSync(); + + try { + await integrationDriver( + onScreenshot: (final screenshotName, final screenshotBytes) async { + File('screenshots/$screenshotName.png').writeAsBytesSync(screenshotBytes); + return true; + }, + ); + } catch (e) { + // ignore: avoid_print + print('Error occurred: $e'); + } +} diff --git a/packages/nextcloud/pubspec.yaml b/packages/nextcloud/pubspec.yaml index 59c241bd..7a2e59a3 100644 --- a/packages/nextcloud/pubspec.yaml +++ b/packages/nextcloud/pubspec.yaml @@ -5,7 +5,7 @@ environment: sdk: '>=2.17.0 <3.0.0' dependencies: - crypto: ^3.0.2 + crypto: ^3.0.1 crypton: ^2.0.5 http: ^0.13.4 intl: ^0.17.0 diff --git a/tool/Dockerfile.dev b/tool/Dockerfile.dev index e113938b..83f939a0 100644 --- a/tool/Dockerfile.dev +++ b/tool/Dockerfile.dev @@ -5,7 +5,7 @@ USER www-data ARG username ARG password RUN ./occ maintenance:install --admin-user admin --admin-pass "$password" --admin-email admin@example.com -RUN OC_PASS="$password" ./occ user:add --password-from-env "$username" +RUN OC_PASS="$password" ./occ user:add --password-from-env --group admin "$username" RUN ./occ app:install news RUN ./occ app:install notes RUN ./occ config:system:set trusted_domains 1 --value=10.0.2.2 diff --git a/tool/build-dev-container-image.sh b/tool/build-dev-container-image.sh new file mode 100755 index 00000000..7c92902c --- /dev/null +++ b/tool/build-dev-container-image.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail +cd "$(dirname "$0")/.." + +docker build -t nextcloud-neon-dev --build-arg "username=$username" --build-arg "password=$password" -f - ./packages/nextcloud/test < tool/Dockerfile.dev diff --git a/tool/common.sh b/tool/common.sh new file mode 100755 index 00000000..98a51666 --- /dev/null +++ b/tool/common.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail + +export username="test" +export password="supersafepasswordtocircumventpasswordpolicies" diff --git a/tool/generate-screenshots.sh b/tool/generate-screenshots.sh new file mode 100755 index 00000000..e130f936 --- /dev/null +++ b/tool/generate-screenshots.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euxo pipefail +cd "$(dirname "$0")/.." + +source tool/common.sh + +./tool/build-dev-container-image.sh +container_id="$(docker run --rm -d -p "80:80" nextcloud-neon-dev)" +function cleanup() { + docker kill "$container_id" +} +trap cleanup EXIT + +( + cd packages/neon + fvm flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/screenshot_test.dart \ + --android-emulator +) diff --git a/tool/run-dev-instance.sh b/tool/run-dev-instance.sh index 3f3d8e55..e54f9c7b 100755 --- a/tool/run-dev-instance.sh +++ b/tool/run-dev-instance.sh @@ -2,8 +2,6 @@ set -euxo pipefail cd "$(dirname "$0")/.." -username="test" -password="supersafepasswordtocircumventpasswordpolicies" ip="" if [ "$#" -ne 1 ]; then echo "You need to give the platform type: localhost, android-emulator" @@ -17,7 +15,8 @@ else exit 1 fi -docker build -t nextcloud-neon-dev --build-arg "username=$username" --build-arg "password=$password" -f - ./packages/nextcloud/test < tool/Dockerfile.dev +source tool/common.sh + echo "TEST_HOST=$ip TEST_USER=$username TEST_PASSWORD=$password" > packages/neon/assets/.env