Browse Source

neon, tool: Automate taking screenshots

pull/32/head
jld3103 2 years ago
parent
commit
e7884a937c
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 7
      packages/neon/README.md
  2. 2
      packages/neon/android/app/build.gradle
  3. 563
      packages/neon/integration_test/screenshot_test.dart
  4. 1
      packages/neon/lib/app.dart
  5. 4
      packages/neon/lib/main.dart
  6. 32
      packages/neon/lib/src/apps/files/widgets/browser_view.dart
  7. 20
      packages/neon/lib/src/apps/news/widgets/feeds_view.dart
  8. 12
      packages/neon/lib/src/apps/news/widgets/folders_view.dart
  9. 6
      packages/neon/lib/src/blocs/capabilities.dart
  10. 7
      packages/neon/lib/src/blocs/capabilities.rxb.g.dart
  11. 18
      packages/neon/lib/src/blocs/push_notifications.dart
  12. 2
      packages/neon/lib/src/models/account.dart
  13. 12
      packages/neon/lib/src/pages/home/home.dart
  14. 6
      packages/neon/lib/src/pages/login/login.dart
  15. 12
      packages/neon/lib/src/pages/settings/settings.dart
  16. 7
      packages/neon/lib/src/utils/global_options.dart
  17. 4
      packages/neon/lib/src/utils/push_utils.dart
  18. 36
      packages/neon/lib/src/utils/request_manager.dart
  19. 94
      packages/neon/pubspec.lock
  20. 6
      packages/neon/pubspec.yaml
  21. BIN
      packages/neon/screenshots/files_actions.png
  22. BIN
      packages/neon/screenshots/files_create.png
  23. BIN
      packages/neon/screenshots/files_details.png
  24. BIN
      packages/neon/screenshots/files_photos.png
  25. BIN
      packages/neon/screenshots/files_root.png
  26. BIN
      packages/neon/screenshots/home_drawer.png
  27. BIN
      packages/neon/screenshots/login.png
  28. BIN
      packages/neon/screenshots/login_form.png
  29. BIN
      packages/neon/screenshots/login_server_selection.png
  30. BIN
      packages/neon/screenshots/news_add_feed.png
  31. BIN
      packages/neon/screenshots/news_articles_feed_list.png
  32. BIN
      packages/neon/screenshots/news_articles_starred_list.png
  33. BIN
      packages/neon/screenshots/news_articles_unread_list.png
  34. BIN
      packages/neon/screenshots/news_feed_add.png
  35. BIN
      packages/neon/screenshots/news_feed_articles_list.png
  36. BIN
      packages/neon/screenshots/news_feeds_list.png
  37. BIN
      packages/neon/screenshots/news_folders_list.png
  38. BIN
      packages/neon/screenshots/notes_categories_list.png
  39. BIN
      packages/neon/screenshots/notes_create.png
  40. BIN
      packages/neon/screenshots/notes_edit.png
  41. BIN
      packages/neon/screenshots/notes_list.png
  42. BIN
      packages/neon/screenshots/notes_note_create.png
  43. BIN
      packages/neon/screenshots/notes_note_edit.png
  44. BIN
      packages/neon/screenshots/notes_note_preview.png
  45. BIN
      packages/neon/screenshots/notes_notes_list.png
  46. BIN
      packages/neon/screenshots/notes_preview.png
  47. BIN
      packages/neon/screenshots/notifications_list.png
  48. BIN
      packages/neon/screenshots/settings_account.png
  49. BIN
      packages/neon/screenshots/settings_accounts.png
  50. BIN
      packages/neon/screenshots/settings_app_files.png
  51. BIN
      packages/neon/screenshots/settings_app_news.png
  52. BIN
      packages/neon/screenshots/settings_app_notes.png
  53. BIN
      packages/neon/screenshots/settings_dark.png
  54. BIN
      packages/neon/screenshots/settings_files.png
  55. BIN
      packages/neon/screenshots/settings_light.png
  56. BIN
      packages/neon/screenshots/settings_news.png
  57. BIN
      packages/neon/screenshots/settings_notes.png
  58. BIN
      packages/neon/screenshots/settings_oled.png
  59. 23
      packages/neon/test_driver/integration_test.dart
  60. 2
      packages/nextcloud/pubspec.yaml
  61. 2
      tool/Dockerfile.dev
  62. 5
      tool/build-dev-container-image.sh
  63. 5
      tool/common.sh
  64. 20
      tool/generate-screenshots.sh
  65. 5
      tool/run-dev-instance.sh

7
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/`. For more screenshots see `./screenshots/`.
| ![](screenshots/login_server_selection.png) | ![](screenshots/settings_oled.png) | ![](screenshots/settings_news.png) | | ![](screenshots/login_server_selection.png) | ![](screenshots/home_drawer.png) | ![](screenshots/settings_oled.png) |
|------------------------------------------------|------------------------------------|------------------------------------| |---------------------------------------------|------------------------------------------------|--------------------------------------|
| ![](screenshots/news_articles_unread_list.png) | ![](screenshots/files_photos.png) | ![](screenshots/notes_edit.png) | | ![](screenshots/files_photos.png) | ![](screenshots/news_articles_unread_list.png) | ![](screenshots/notes_note_edit.png) |
| ![](screenshots/notifications_list.png) | | |

2
packages/neon/android/app/build.gradle

@ -46,7 +46,7 @@ android {
applicationId "de.provokateurin.neon" applicationId "de.provokateurin.neon"
// You can update the following values to match your application needs. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

563
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 = <String, dynamic>{};
@override
Future<bool> clear() async {
_data.clear();
return true;
}
@override
Future<bool> commit() async => true;
@override
Future reload() async {}
@override
Future<bool> remove(String key) async {
_data.remove(key);
return true;
}
@override
Set<String> 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<String>? getStringList(String key) => (_data[key] as List).cast<String>();
@override
Future<bool> setBool(String key, bool value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setDouble(String key, double value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setInt(String key, int value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setString(String key, String value) async {
_data[key] = value;
return true;
}
@override
Future<bool> setStringList(String key, List<String> 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<NextcloudTheme>();
await tester.pumpWidget(
MultiProvider(
providers: [
Provider<SharedPreferences>(
create: (final _) => sharedPreferences,
),
Provider<Env?>(
create: (final _) => null,
),
Provider<NeonPlatform>(
create: (final _) => platform,
),
Provider<GlobalOptions>(
create: (final _) => globalOptions,
),
Provider<RequestManager>(
create: (final _) => requestManager,
),
Provider<AccountsBloc>(
create: (final _) => accountsBloc,
),
Provider<PushNotificationsBloc>(
create: (final _) => pushNotificationsBloc,
),
Provider<List<AppImplementation>>(
create: (final _) => allAppImplementations,
),
Provider<PackageInfo>(
create: (final _) => packageInfo,
),
],
child: StreamBuilder<NextcloudTheme>(
stream: userThemeStream,
builder: (final context, final themeSnapshot) => StreamBuilder<ThemeMode>(
stream: globalOptions.themeMode.stream,
builder: (final context, final themeModeSnapshot) => StreamBuilder<bool>(
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<FilesFileAction>).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<SettingsAccountAction>));
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');
});
}

1
packages/neon/lib/app.dart

@ -106,7 +106,6 @@ class _NeonAppState extends State<NeonApp> with WidgetsBindingObserver {
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: _navigatorKey, navigatorKey: _navigatorKey,
debugShowCheckedModeBanner: false,
theme: getThemeFromNextcloudTheme( theme: getThemeFromNextcloudTheme(
_userTheme, _userTheme,
themeModeSnapshot.data!, themeModeSnapshot.data!,

4
packages/neon/lib/main.dart

@ -29,7 +29,9 @@ Future main() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await SharedPreferences.getInstance();
final platform = await getNeonPlatform(); 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 allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform);
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();

32
packages/neon/lib/src/apps/files/widgets/browser_view.dart

@ -319,11 +319,11 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
), ),
), ),
trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions
? PopupMenuButton<_FileAction>( ? PopupMenuButton<FilesFileAction>(
itemBuilder: (final context) => [ itemBuilder: (final context) => [
if (details.isFavorite != null) ...[ if (details.isFavorite != null) ...[
PopupMenuItem( PopupMenuItem(
value: _FileAction.toggleFavorite, value: FilesFileAction.toggleFavorite,
child: Text( child: Text(
details.isFavorite! details.isFavorite!
? AppLocalizations.of(context).filesRemoveFromFavorites ? AppLocalizations.of(context).filesRemoveFromFavorites
@ -332,43 +332,43 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
), ),
], ],
PopupMenuItem( PopupMenuItem(
value: _FileAction.details, value: FilesFileAction.details,
child: Text(AppLocalizations.of(context).filesDetails), child: Text(AppLocalizations.of(context).filesDetails),
), ),
PopupMenuItem( PopupMenuItem(
value: _FileAction.rename, value: FilesFileAction.rename,
child: Text(AppLocalizations.of(context).rename), child: Text(AppLocalizations.of(context).rename),
), ),
PopupMenuItem( PopupMenuItem(
value: _FileAction.move, value: FilesFileAction.move,
child: Text(AppLocalizations.of(context).move), child: Text(AppLocalizations.of(context).move),
), ),
PopupMenuItem( PopupMenuItem(
value: _FileAction.copy, value: FilesFileAction.copy,
child: Text(AppLocalizations.of(context).copy), child: Text(AppLocalizations.of(context).copy),
), ),
// TODO: https://github.com/jld3103/nextcloud-neon/issues/4 // TODO: https://github.com/jld3103/nextcloud-neon/issues/4
if (!details.isDirectory) ...[ if (!details.isDirectory) ...[
PopupMenuItem( PopupMenuItem(
value: _FileAction.sync, value: FilesFileAction.sync,
child: Text(AppLocalizations.of(context).filesSync), child: Text(AppLocalizations.of(context).filesSync),
), ),
], ],
PopupMenuItem( PopupMenuItem(
value: _FileAction.delete, value: FilesFileAction.delete,
child: Text(AppLocalizations.of(context).delete), child: Text(AppLocalizations.of(context).delete),
), ),
], ],
onSelected: (final action) async { onSelected: (final action) async {
switch (action) { switch (action) {
case _FileAction.toggleFavorite: case FilesFileAction.toggleFavorite:
if (details.isFavorite ?? false) { if (details.isFavorite ?? false) {
widget.filesBloc.removeFavorite(details.path); widget.filesBloc.removeFavorite(details.path);
} else { } else {
widget.filesBloc.addFavorite(details.path); widget.filesBloc.addFavorite(details.path);
} }
break; break;
case _FileAction.details: case FilesFileAction.details:
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => FilesDetailsPage( builder: (final context) => FilesDetailsPage(
@ -378,7 +378,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
), ),
); );
break; break;
case _FileAction.rename: case FilesFileAction.rename:
final result = await showRenameDialog( final result = await showRenameDialog(
context: context, context: context,
title: details.isDirectory title: details.isDirectory
@ -390,7 +390,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
widget.filesBloc.rename(details.path, result); widget.filesBloc.rename(details.path, result);
} }
break; break;
case _FileAction.move: case FilesFileAction.move:
final b = widget.filesBloc.getNewFilesBrowserBloc(); final b = widget.filesBloc.getNewFilesBrowserBloc();
final originalPath = details.path.sublist(0, details.path.length - 1); final originalPath = details.path.sublist(0, details.path.length - 1);
b.setPath(originalPath); b.setPath(originalPath);
@ -407,7 +407,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
widget.filesBloc.move(details.path, result..add(details.name)); widget.filesBloc.move(details.path, result..add(details.name));
} }
break; break;
case _FileAction.copy: case FilesFileAction.copy:
final b = widget.filesBloc.getNewFilesBrowserBloc(); final b = widget.filesBloc.getNewFilesBrowserBloc();
final originalPath = details.path.sublist(0, details.path.length - 1); final originalPath = details.path.sublist(0, details.path.length - 1);
b.setPath(originalPath); b.setPath(originalPath);
@ -424,7 +424,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
widget.filesBloc.copy(details.path, result..add(details.name)); widget.filesBloc.copy(details.path, result..add(details.name));
} }
break; break;
case _FileAction.sync: case FilesFileAction.sync:
final sizeWarning = widget.bloc.options.downloadSizeWarning.value; final sizeWarning = widget.bloc.options.downloadSizeWarning.value;
if (sizeWarning != null && details.size > sizeWarning) { if (sizeWarning != null && details.size > sizeWarning) {
if (!(await showConfirmationDialog( if (!(await showConfirmationDialog(
@ -439,7 +439,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
} }
widget.filesBloc.syncFile(details.path); widget.filesBloc.syncFile(details.path);
break; break;
case _FileAction.delete: case FilesFileAction.delete:
if (await showConfirmationDialog( if (await showConfirmationDialog(
context, context,
details.isDirectory details.isDirectory
@ -459,7 +459,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
); );
} }
enum _FileAction { enum FilesFileAction {
toggleFavorite, toggleFavorite,
details, details,
rename, rename,

20
packages/neon/lib/src/apps/news/widgets/feeds_view.dart

@ -140,30 +140,30 @@ class NewsFeedsView extends StatelessWidget {
), ),
), ),
], ],
PopupMenuButton<_FeedAction>( PopupMenuButton<NewsFeedAction>(
itemBuilder: (final context) => [ itemBuilder: (final context) => [
PopupMenuItem( PopupMenuItem(
value: _FeedAction.showURL, value: NewsFeedAction.showURL,
child: Text(AppLocalizations.of(context).newsShowFeedURL), child: Text(AppLocalizations.of(context).newsShowFeedURL),
), ),
PopupMenuItem( PopupMenuItem(
value: _FeedAction.delete, value: NewsFeedAction.delete,
child: Text(AppLocalizations.of(context).delete), child: Text(AppLocalizations.of(context).delete),
), ),
PopupMenuItem( PopupMenuItem(
value: _FeedAction.rename, value: NewsFeedAction.rename,
child: Text(AppLocalizations.of(context).rename), child: Text(AppLocalizations.of(context).rename),
), ),
if (folders.isNotEmpty) ...[ if (folders.isNotEmpty) ...[
PopupMenuItem( PopupMenuItem(
value: _FeedAction.move, value: NewsFeedAction.move,
child: Text(AppLocalizations.of(context).move), child: Text(AppLocalizations.of(context).move),
), ),
], ],
], ],
onSelected: (final action) async { onSelected: (final action) async {
switch (action) { switch (action) {
case _FeedAction.showURL: case NewsFeedAction.showURL:
await showDialog( await showDialog(
context: context, context: context,
builder: (final context) => NewsFeedShowURLDialog( builder: (final context) => NewsFeedShowURLDialog(
@ -171,7 +171,7 @@ class NewsFeedsView extends StatelessWidget {
), ),
); );
break; break;
case _FeedAction.delete: case NewsFeedAction.delete:
if (await showConfirmationDialog( if (await showConfirmationDialog(
context, context,
AppLocalizations.of(context).newsRemoveFeedConfirm(feed.title!), AppLocalizations.of(context).newsRemoveFeedConfirm(feed.title!),
@ -179,7 +179,7 @@ class NewsFeedsView extends StatelessWidget {
bloc.removeFeed(feed.id!); bloc.removeFeed(feed.id!);
} }
break; break;
case _FeedAction.rename: case NewsFeedAction.rename:
final result = await showRenameDialog( final result = await showRenameDialog(
context: context, context: context,
title: AppLocalizations.of(context).newsRenameFeed, title: AppLocalizations.of(context).newsRenameFeed,
@ -189,7 +189,7 @@ class NewsFeedsView extends StatelessWidget {
bloc.renameFeed(feed.id!, result); bloc.renameFeed(feed.id!, result);
} }
break; break;
case _FeedAction.move: case NewsFeedAction.move:
final result = await showDialog<List<int?>>( final result = await showDialog<List<int?>>(
context: context, context: context,
builder: (final context) => NewsMoveFeedDialog( builder: (final context) => NewsMoveFeedDialog(
@ -224,7 +224,7 @@ class NewsFeedsView extends StatelessWidget {
); );
} }
enum _FeedAction { enum NewsFeedAction {
showURL, showURL,
delete, delete,
rename, rename,

12
packages/neon/lib/src/apps/news/widgets/folders_view.dart

@ -131,20 +131,20 @@ class NewsFoldersView extends StatelessWidget {
], ],
), ),
), ),
trailing: PopupMenuButton<_FolderAction>( trailing: PopupMenuButton<NewsFolderAction>(
itemBuilder: (final context) => [ itemBuilder: (final context) => [
PopupMenuItem( PopupMenuItem(
value: _FolderAction.delete, value: NewsFolderAction.delete,
child: Text(AppLocalizations.of(context).delete), child: Text(AppLocalizations.of(context).delete),
), ),
PopupMenuItem( PopupMenuItem(
value: _FolderAction.rename, value: NewsFolderAction.rename,
child: Text(AppLocalizations.of(context).rename), child: Text(AppLocalizations.of(context).rename),
), ),
], ],
onSelected: (final action) async { onSelected: (final action) async {
switch (action) { switch (action) {
case _FolderAction.delete: case NewsFolderAction.delete:
if (await showConfirmationDialog( if (await showConfirmationDialog(
context, context,
AppLocalizations.of(context).newsDeleteFolderConfirm(folderFeedsWrapper.folder.name!), AppLocalizations.of(context).newsDeleteFolderConfirm(folderFeedsWrapper.folder.name!),
@ -152,7 +152,7 @@ class NewsFoldersView extends StatelessWidget {
bloc.deleteFolder(folderFeedsWrapper.folder.id!); bloc.deleteFolder(folderFeedsWrapper.folder.id!);
} }
break; break;
case _FolderAction.rename: case NewsFolderAction.rename:
final result = await showRenameDialog( final result = await showRenameDialog(
context: context, context: context,
title: AppLocalizations.of(context).newsRenameFolder, title: AppLocalizations.of(context).newsRenameFolder,
@ -184,7 +184,7 @@ class NewsFoldersView extends StatelessWidget {
} }
} }
enum _FolderAction { enum NewsFolderAction {
delete, delete,
rename, rename,
} }

6
packages/neon/lib/src/blocs/capabilities.dart

@ -9,7 +9,9 @@ part 'capabilities.rxb.g.dart';
typedef Capabilities = CoreServerCapabilitiesOcsData; typedef Capabilities = CoreServerCapabilitiesOcsData;
typedef NextcloudTheme = CoreServerCapabilitiesOcsDataCapabilitiesTheming; typedef NextcloudTheme = CoreServerCapabilitiesOcsDataCapabilitiesTheming;
abstract class CapabilitiesBlocEvents {} abstract class CapabilitiesBlocEvents {
void refresh();
}
abstract class CapabilitiesBlocStates { abstract class CapabilitiesBlocStates {
BehaviorSubject<Result<Capabilities>> get capabilities; BehaviorSubject<Result<Capabilities>> get capabilities;
@ -21,6 +23,8 @@ class CapabilitiesBloc extends $CapabilitiesBloc {
this._requestManager, this._requestManager,
this._client, this._client,
) { ) {
_$refreshEvent.listen((final _) => _loadCapabilities());
_loadCapabilities(); _loadCapabilities();
} }

7
packages/neon/lib/src/blocs/capabilities.rxb.g.dart

@ -19,9 +19,15 @@ abstract class $CapabilitiesBloc extends RxBlocBase
implements CapabilitiesBlocEvents, CapabilitiesBlocStates, CapabilitiesBlocType { implements CapabilitiesBlocEvents, CapabilitiesBlocStates, CapabilitiesBlocType {
final _compositeSubscription = CompositeSubscription(); final _compositeSubscription = CompositeSubscription();
/// Тhe [Subject] where events sink to by calling [refresh]
final _$refreshEvent = PublishSubject<void>();
/// The state of [capabilities] implemented in [_mapToCapabilitiesState] /// The state of [capabilities] implemented in [_mapToCapabilitiesState]
late final BehaviorSubject<Result<CoreServerCapabilitiesOcsData>> _capabilitiesState = _mapToCapabilitiesState(); late final BehaviorSubject<Result<CoreServerCapabilitiesOcsData>> _capabilitiesState = _mapToCapabilitiesState();
@override
void refresh() => _$refreshEvent.add(null);
@override @override
BehaviorSubject<Result<CoreServerCapabilitiesOcsData>> get capabilities => _capabilitiesState; BehaviorSubject<Result<CoreServerCapabilitiesOcsData>> get capabilities => _capabilitiesState;
@ -35,6 +41,7 @@ abstract class $CapabilitiesBloc extends RxBlocBase
@override @override
void dispose() { void dispose() {
_$refreshEvent.close();
_compositeSubscription.dispose(); _compositeSubscription.dispose();
super.dispose(); super.dispose();
} }

18
packages/neon/lib/src/blocs/push_notifications.dart

@ -28,9 +28,18 @@ class PushNotificationsBloc extends $PushNotificationsBloc {
this._platform, this._platform,
) { ) {
if (_platform.canUsePushNotifications) { if (_platform.canUsePushNotifications) {
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 // We just use a single RSA keypair for all accounts
_keypair = PushUtils.loadRSAKeypair(_storage); _keypair = PushUtils.loadRSAKeypair(_storage);
_setupUnifiedPush(); await _setupUnifiedPush();
}
}
});
} }
} }
@ -81,7 +90,6 @@ class PushNotificationsBloc extends $PushNotificationsBloc {
}, },
onMessage: PushUtils.onMessage, onMessage: PushUtils.onMessage,
); );
await _globalOptions.updateDistributors(await UnifiedPush.getDistributors());
_globalOptions.pushNotificationsDistributor.stream.listen((final distributor) async { _globalOptions.pushNotificationsDistributor.stream.listen((final distributor) async {
final disabled = distributor == null; final disabled = distributor == null;
@ -109,7 +117,8 @@ class PushNotificationsBloc extends $PushNotificationsBloc {
} }
Future _registerUnifiedPushInstances(final List<Account> accounts) async { Future _registerUnifiedPushInstances(final List<Account> 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); await UnifiedPush.registerApp(account.client.id);
} }
} }
@ -120,7 +129,8 @@ class PushNotificationsBloc extends $PushNotificationsBloc {
late final _storage = Storage('notifications', _sharedPreferences); late final _storage = Storage('notifications', _sharedPreferences);
final GlobalOptions _globalOptions; final GlobalOptions _globalOptions;
final Env? _env; final Env? _env;
late final RSAKeypair? _keypair; RSAKeypair? _keypair;
bool? _pushNotificationsEnabled;
String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}'; String _keyLastEndpoint(final Account account) => 'last-endpoint-${account.id}';

2
packages/neon/lib/src/models/account.dart

@ -54,7 +54,7 @@ class Account {
_client ??= NextcloudClient( _client ??= NextcloudClient(
serverURL, serverURL,
username: username, username: username,
password: password ?? appPassword, password: appPassword ?? password,
userAgentOverride: userAgent(packageInfo), userAgentOverride: userAgent(packageInfo),
); );
} }

12
packages/neon/lib/src/pages/home/home.dart

@ -449,7 +449,15 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
], ],
] else ...[ ] else ...[
Container(), ExceptionWidget(
capabilitiesError,
onRetry: () {
_capabilitiesBloc.refresh();
},
),
CustomLinearProgressIndicator(
visible: capabilitiesLoading,
),
], ],
if (accountsSnapshot.data!.length != 1) ...[ if (accountsSnapshot.data!.length != 1) ...[
DropdownButtonHideUnderline( DropdownButtonHideUnderline(
@ -499,6 +507,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
for (final appImplementation in appsData) ...[ for (final appImplementation in appsData) ...[
if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[ if (appsData.map((final a) => a.id).contains(appImplementation.id)) ...[
ListTile( ListTile(
key: Key('app-${appImplementation.id}'),
title: Text(appImplementation.name(context)), title: Text(appImplementation.name(context)),
leading: appImplementation.buildIcon(context), leading: appImplementation.buildIcon(context),
minLeadingWidth: 0, minLeadingWidth: 0,
@ -515,6 +524,7 @@ class _HomePageState extends State<HomePage> with tray.TrayListener, WindowListe
), ),
), ),
ListTile( ListTile(
key: const Key('settings'),
title: Text(AppLocalizations.of(context).settings), title: Text(AppLocalizations.of(context).settings),
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
minLeadingWidth: 0, minLeadingWidth: 0,

6
packages/neon/lib/src/pages/login/login.dart

@ -84,6 +84,12 @@ class _LoginPageState extends State<LoginPage> {
}, },
}; };
@override
void dispose() {
_loginBloc.dispose();
super.dispose();
}
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final env = Provider.of<Env?>(context); final env = Provider.of<Env?>(context);

12
packages/neon/lib/src/pages/settings/settings.dart

@ -168,20 +168,20 @@ class _SettingsPageState extends State<SettingsPage> {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
PopupMenuButton<_AccountAction>( PopupMenuButton<SettingsAccountAction>(
itemBuilder: (final context) => [ itemBuilder: (final context) => [
PopupMenuItem( PopupMenuItem(
value: _AccountAction.settings, value: SettingsAccountAction.settings,
child: Text(AppLocalizations.of(context).settings), child: Text(AppLocalizations.of(context).settings),
), ),
PopupMenuItem( PopupMenuItem(
value: _AccountAction.delete, value: SettingsAccountAction.delete,
child: Text(AppLocalizations.of(context).delete), child: Text(AppLocalizations.of(context).delete),
), ),
], ],
onSelected: (final action) async { onSelected: (final action) async {
switch (action) { switch (action) {
case _AccountAction.settings: case SettingsAccountAction.settings:
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => AccountSpecificSettingsPage( builder: (final context) => AccountSpecificSettingsPage(
@ -191,7 +191,7 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
); );
break; break;
case _AccountAction.delete: case SettingsAccountAction.delete:
if (await showConfirmationDialog( if (await showConfirmationDialog(
context, context,
AppLocalizations.of(context).globalOptionsAccountsRemoveConfirm( AppLocalizations.of(context).globalOptionsAccountsRemoveConfirm(
@ -328,7 +328,7 @@ class _SettingsPageState extends State<SettingsPage> {
} }
} }
enum _AccountAction { enum SettingsAccountAction {
settings, settings,
delete, delete,
} }

7
packages/neon/lib/src/utils/global_options.dart

@ -10,8 +10,7 @@ class GlobalOptions {
}); });
_pushNotificationsDistributorsSubject.listen((final distributors) async { _pushNotificationsDistributorsSubject.listen((final distributors) async {
_pushNotificationsEnabledSubject.add(distributors.isNotEmpty); _pushNotificationsEnabledEnabledSubject.add(distributors.isNotEmpty);
await pushNotificationsEnabled.set(distributors.isNotEmpty);
await _setDefaultDistributor(); await _setDefaultDistributor();
}); });
@ -35,7 +34,7 @@ class GlobalOptions {
final PackageInfo _packageInfo; final PackageInfo _packageInfo;
final _accountsIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>(); final _accountsIDsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
final _themeOLEDAsDarkEnabledSubject = BehaviorSubject<bool>(); final _themeOLEDAsDarkEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsEnabledSubject = BehaviorSubject<bool>(); final _pushNotificationsEnabledEnabledSubject = BehaviorSubject<bool>();
final _pushNotificationsDistributorsSubject = BehaviorSubject<Map<String?, LabelBuilder>>(); final _pushNotificationsDistributorsSubject = BehaviorSubject<Map<String?, LabelBuilder>>();
late final _distributorsMap = <String, String Function(BuildContext)>{ late final _distributorsMap = <String, String Function(BuildContext)>{
@ -128,7 +127,7 @@ class GlobalOptions {
key: 'push-notifications-enabled', key: 'push-notifications-enabled',
label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled, label: (final context) => AppLocalizations.of(context).globalOptionsPushNotificationsEnabled,
defaultValue: BehaviorSubject.seeded(true), defaultValue: BehaviorSubject.seeded(true),
enabled: _pushNotificationsEnabledSubject, enabled: _pushNotificationsEnabledEnabledSubject,
); );
late final pushNotificationsDistributor = SelectOption<String?>( late final pushNotificationsDistributor = SelectOption<String?>(

4
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 localizations = await AppLocalizations.delegate.load(Locale(parts[0], parts.length > 1 ? parts[1] : null));
final platform = await getNeonPlatform(); 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 allAppImplementations = getAppImplementations(sharedPreferences, requestManager, platform);
final matchingAppImplementations = final matchingAppImplementations =

36
packages/neon/lib/src/utils/request_manager.dart

@ -1,15 +1,11 @@
part of '../neon.dart'; part of '../neon.dart';
Future<RequestManager> getRequestManager(NeonPlatform platform) async {
final cache = Cache(platform);
await cache.init();
return RequestManager(cache);
}
class RequestManager { class RequestManager {
RequestManager(this._cache); RequestManager([
this.cache,
]);
final Cache _cache; final Cache? cache;
final bool _enablePrinting = false; final bool _enablePrinting = false;
@ -142,9 +138,9 @@ class RequestManager {
_print('[Request]: $k'); _print('[Request]: $k');
if ((preferCache || preloadCache) && await _cache.has(key)) { if ((preferCache || preloadCache) && cache != null && await cache!.has(key)) {
_print('[Cache]: $k'); _print('[Cache]: $k');
final s = unwrap(await deserialize((await _cache.get(key))!)); final s = unwrap(await deserialize((await cache!.get(key))!));
if (preloadCache) { if (preloadCache) {
yield ResultCached(s, loading: true); yield ResultCached(s, loading: true);
} else { } else {
@ -158,14 +154,14 @@ class RequestManager {
final s = await serialize(response); final s = await serialize(response);
_print('[Response]: $k'); _print('[Response]: $k');
await _cache.set(key, s); await cache?.set(key, s);
yield Result.success(unwrap(response)); yield Result.success(unwrap(response));
} on Exception catch (e) { } on Exception catch (e) {
if (await _cache.has(key)) { if (cache != null && await cache!.has(key)) {
_print('[Cache]: $k'); _print('[Cache]: $k');
debugPrint(e.toString()); 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; return;
} }
_print('[Failure]: $k'); _print('[Failure]: $k');
@ -261,7 +257,7 @@ class ResultCached<T> implements Result<T> {
String tag; String tag;
} }
extension ResultData<T> on Result<T> { extension ResultDataError<T> on Result<T> {
T? get data { T? get data {
if (this is ResultSuccess<T>) { if (this is ResultSuccess<T>) {
return (this as ResultSuccess<T>).data; return (this as ResultSuccess<T>).data;
@ -273,6 +269,18 @@ extension ResultData<T> on Result<T> {
return null; return null;
} }
Exception? get error {
if (this is ResultError<T>) {
return (this as ResultError<T>).error;
}
if (this is ResultCached<T>) {
return (this as ResultCached<T>).error;
}
return null;
}
} }
extension ListAs<T, V> on List<T> { extension ListAs<T, V> on List<T> {

94
packages/neon/pubspec.lock

@ -7,21 +7,21 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "41.0.0" version: "42.0.0"
analyzer: analyzer:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.0" version: "4.3.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.0" version: "3.1.11"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -42,7 +42,14 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: build:
dependency: transitive dependency: transitive
description: description:
@ -161,7 +168,7 @@ packages:
name: crypto name: crypto
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.0.1"
crypton: crypton:
dependency: transitive dependency: transitive
description: description:
@ -190,6 +197,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.4" version: "0.7.4"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -244,6 +258,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.2" version: "5.0.2"
flutter_driver:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_file_dialog: flutter_file_dialog:
dependency: "direct main" dependency: "direct main"
description: description:
@ -290,14 +309,14 @@ packages:
name: flutter_markdown name: flutter_markdown
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.10+2" version: "0.6.10+3"
flutter_native_splash: flutter_native_splash:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_native_splash name: flutter_native_splash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.4" version: "2.2.5"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -319,6 +338,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1+1" version: "1.1.1+1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -331,6 +355,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -415,6 +444,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
intersperse: intersperse:
dependency: "direct main" dependency: "direct main"
description: description:
@ -484,7 +518,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.12" version: "0.12.11"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -738,7 +772,7 @@ packages:
name: pointycastle name: pointycastle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.6.0" version: "3.6.1"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -829,7 +863,7 @@ packages:
name: rxdart name: rxdart
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.27.4" version: "0.27.5"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@ -995,7 +1029,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.9.0" version: "1.8.2"
sqflite: sqflite:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1051,7 +1085,14 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: synchronized:
dependency: transitive dependency: transitive
description: description:
@ -1065,7 +1106,14 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted 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: timezone:
dependency: transitive dependency: transitive
description: description:
@ -1093,7 +1141,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.0"
unifiedpush: unifiedpush:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1185,6 +1233,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "8.2.2"
wakelock: wakelock:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1234,6 +1289,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
webview_flutter: webview_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1247,7 +1309,7 @@ packages:
name: webview_flutter_android name: webview_flutter_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.14" version: "2.9.2"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1261,7 +1323,7 @@ packages:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.9.0" version: "2.9.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:

6
packages/neon/pubspec.yaml

@ -62,6 +62,12 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.1.7 build_runner: ^2.1.7
flutter_driver:
sdk: flutter
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
json_serializable: ^6.1.4 json_serializable: ^6.1.4
nit_picking: nit_picking:
git: git:

BIN
packages/neon/screenshots/files_actions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

BIN
packages/neon/screenshots/files_create.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 274 KiB

BIN
packages/neon/screenshots/files_details.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 KiB

After

Width:  |  Height:  |  Size: 753 KiB

BIN
packages/neon/screenshots/files_photos.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

After

Width:  |  Height:  |  Size: 324 KiB

BIN
packages/neon/screenshots/files_root.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 138 KiB

BIN
packages/neon/screenshots/home_drawer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
packages/neon/screenshots/login.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 918 KiB

BIN
packages/neon/screenshots/login_form.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

BIN
packages/neon/screenshots/login_server_selection.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
packages/neon/screenshots/news_add_feed.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

BIN
packages/neon/screenshots/news_articles_feed_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

BIN
packages/neon/screenshots/news_articles_starred_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 101 KiB

BIN
packages/neon/screenshots/news_articles_unread_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 271 KiB

BIN
packages/neon/screenshots/news_feed_add.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
packages/neon/screenshots/news_feed_articles_list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

BIN
packages/neon/screenshots/news_feeds_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 95 KiB

BIN
packages/neon/screenshots/news_folders_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

BIN
packages/neon/screenshots/notes_categories_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
packages/neon/screenshots/notes_create.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

BIN
packages/neon/screenshots/notes_edit.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

BIN
packages/neon/screenshots/notes_list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

BIN
packages/neon/screenshots/notes_note_create.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
packages/neon/screenshots/notes_note_edit.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
packages/neon/screenshots/notes_note_preview.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
packages/neon/screenshots/notes_notes_list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
packages/neon/screenshots/notes_preview.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

BIN
packages/neon/screenshots/notifications_list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
packages/neon/screenshots/settings_account.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
packages/neon/screenshots/settings_accounts.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
packages/neon/screenshots/settings_app_files.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
packages/neon/screenshots/settings_app_news.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
packages/neon/screenshots/settings_app_notes.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
packages/neon/screenshots/settings_dark.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
packages/neon/screenshots/settings_files.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

BIN
packages/neon/screenshots/settings_light.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 140 KiB

BIN
packages/neon/screenshots/settings_news.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

BIN
packages/neon/screenshots/settings_notes.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

BIN
packages/neon/screenshots/settings_oled.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 135 KiB

23
packages/neon/test_driver/integration_test.dart

@ -0,0 +1,23 @@
import 'dart:io';
import 'package:integration_test/integration_test_driver_extended.dart';
Future<void> 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');
}
}

2
packages/nextcloud/pubspec.yaml

@ -5,7 +5,7 @@ environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.17.0 <3.0.0'
dependencies: dependencies:
crypto: ^3.0.2 crypto: ^3.0.1
crypton: ^2.0.5 crypton: ^2.0.5
http: ^0.13.4 http: ^0.13.4
intl: ^0.17.0 intl: ^0.17.0

2
tool/Dockerfile.dev

@ -5,7 +5,7 @@ USER www-data
ARG username ARG username
ARG password ARG password
RUN ./occ maintenance:install --admin-user admin --admin-pass "$password" --admin-email admin@example.com 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 news
RUN ./occ app:install notes RUN ./occ app:install notes
RUN ./occ config:system:set trusted_domains 1 --value=10.0.2.2 RUN ./occ config:system:set trusted_domains 1 --value=10.0.2.2

5
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

5
tool/common.sh

@ -0,0 +1,5 @@
#!/bin/bash
set -euxo pipefail
export username="test"
export password="supersafepasswordtocircumventpasswordpolicies"

20
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
)

5
tool/run-dev-instance.sh

@ -2,8 +2,6 @@
set -euxo pipefail set -euxo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
username="test"
password="supersafepasswordtocircumventpasswordpolicies"
ip="" ip=""
if [ "$#" -ne 1 ]; then if [ "$#" -ne 1 ]; then
echo "You need to give the platform type: localhost, android-emulator" echo "You need to give the platform type: localhost, android-emulator"
@ -17,7 +15,8 @@ else
exit 1 exit 1
fi 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 echo "TEST_HOST=$ip
TEST_USER=$username TEST_USER=$username
TEST_PASSWORD=$password" > packages/neon/assets/.env TEST_PASSWORD=$password" > packages/neon/assets/.env

Loading…
Cancel
Save