From 96dfed0e22c65de0be3d104d11447c509db0f8c6 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 1 Oct 2023 22:01:26 +0200 Subject: [PATCH 1/4] feat(neon_dashboard): Init Signed-off-by: jld3103 --- .cspell/dart_flutter.txt | 1 + commitlint.yaml | 1 + packages/neon/neon_dashboard/.metadata | 10 + packages/neon/neon_dashboard/LICENSE | 1 + .../neon/neon_dashboard/analysis_options.yaml | 5 + .../neon/neon_dashboard/assets/app.svg.vec | Bin 0 -> 292 bytes packages/neon/neon_dashboard/build.yaml | 0 packages/neon/neon_dashboard/l10n.yaml | 6 + packages/neon/neon_dashboard/lib/l10n/en.arb | 4 + .../lib/l10n/localizations.dart | 125 ++++++ .../lib/l10n/localizations_en.dart | 9 + .../neon_dashboard/lib/neon_dashboard.dart | 1 + packages/neon/neon_dashboard/lib/src/app.dart | 44 ++ .../lib/src/blocs/dashboard.dart | 103 +++++ .../neon/neon_dashboard/lib/src/options.dart | 10 + .../neon_dashboard/lib/src/pages/main.dart | 55 +++ .../neon/neon_dashboard/lib/src/routes.dart | 21 + .../neon/neon_dashboard/lib/src/routes.g.dart | 33 ++ .../neon_dashboard/lib/src/utils/find.dart | 15 + .../lib/src/widgets/widget.dart | 101 +++++ .../lib/src/widgets/widget_button.dart | 43 ++ .../lib/src/widgets/widget_item.dart | 67 +++ packages/neon/neon_dashboard/pubspec.yaml | 43 ++ .../neon_dashboard/pubspec_overrides.yaml | 12 + .../neon/neon_dashboard/test/find_test.dart | 30 ++ .../neon_dashboard/test/goldens/widget.png | Bin 0 -> 4792 bytes .../test/goldens/widget_button_invalid.png | Bin 0 -> 2959 bytes .../test/goldens/widget_button_more.png | Bin 0 -> 3006 bytes .../test/goldens/widget_button_new.png | Bin 0 -> 3006 bytes .../test/goldens/widget_button_setup.png | Bin 0 -> 3006 bytes .../test/goldens/widget_item.png | Bin 0 -> 2771 bytes .../test/goldens/widget_item_not_round.png | Bin 0 -> 2437 bytes .../test/goldens/widget_not_round.png | Bin 0 -> 4792 bytes .../test/goldens/widget_with_empty.png | Bin 0 -> 4885 bytes .../test/goldens/widget_with_half_empty.png | Bin 0 -> 4890 bytes .../test/goldens/widget_without_buttons.png | Bin 0 -> 3746 bytes .../test/goldens/widget_without_items.png | Bin 0 -> 4782 bytes .../neon/neon_dashboard/test/widget_test.dart | 407 ++++++++++++++++++ 38 files changed, 1147 insertions(+) create mode 100644 packages/neon/neon_dashboard/.metadata create mode 120000 packages/neon/neon_dashboard/LICENSE create mode 100644 packages/neon/neon_dashboard/analysis_options.yaml create mode 100644 packages/neon/neon_dashboard/assets/app.svg.vec create mode 100644 packages/neon/neon_dashboard/build.yaml create mode 100644 packages/neon/neon_dashboard/l10n.yaml create mode 100644 packages/neon/neon_dashboard/lib/l10n/en.arb create mode 100644 packages/neon/neon_dashboard/lib/l10n/localizations.dart create mode 100644 packages/neon/neon_dashboard/lib/l10n/localizations_en.dart create mode 100644 packages/neon/neon_dashboard/lib/neon_dashboard.dart create mode 100644 packages/neon/neon_dashboard/lib/src/app.dart create mode 100644 packages/neon/neon_dashboard/lib/src/blocs/dashboard.dart create mode 100644 packages/neon/neon_dashboard/lib/src/options.dart create mode 100644 packages/neon/neon_dashboard/lib/src/pages/main.dart create mode 100644 packages/neon/neon_dashboard/lib/src/routes.dart create mode 100644 packages/neon/neon_dashboard/lib/src/routes.g.dart create mode 100644 packages/neon/neon_dashboard/lib/src/utils/find.dart create mode 100644 packages/neon/neon_dashboard/lib/src/widgets/widget.dart create mode 100644 packages/neon/neon_dashboard/lib/src/widgets/widget_button.dart create mode 100644 packages/neon/neon_dashboard/lib/src/widgets/widget_item.dart create mode 100644 packages/neon/neon_dashboard/pubspec.yaml create mode 100644 packages/neon/neon_dashboard/pubspec_overrides.yaml create mode 100644 packages/neon/neon_dashboard/test/find_test.dart create mode 100644 packages/neon/neon_dashboard/test/goldens/widget.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_button_invalid.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_button_more.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_button_new.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_button_setup.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_item.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_item_not_round.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_not_round.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_with_empty.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_with_half_empty.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_without_buttons.png create mode 100644 packages/neon/neon_dashboard/test/goldens/widget_without_items.png create mode 100644 packages/neon/neon_dashboard/test/widget_test.dart diff --git a/.cspell/dart_flutter.txt b/.cspell/dart_flutter.txt index 82f3cf3e..f1f2d865 100644 --- a/.cspell/dart_flutter.txt +++ b/.cspell/dart_flutter.txt @@ -2,6 +2,7 @@ autofocus endtemplate expando gapless +goldens lerp pubspec sublist diff --git a/commitlint.yaml b/commitlint.yaml index 8aea72a8..bce3939e 100644 --- a/commitlint.yaml +++ b/commitlint.yaml @@ -16,6 +16,7 @@ rules: - dynamite_runtime - file_icons - neon + - neon_dashboard - neon_files - neon_news - neon_notes diff --git a/packages/neon/neon_dashboard/.metadata b/packages/neon/neon_dashboard/.metadata new file mode 100644 index 00000000..9d75c7ec --- /dev/null +++ b/packages/neon/neon_dashboard/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ead455963c12b453cdb2358cad34969c76daf180" + channel: "stable" + +project_type: package diff --git a/packages/neon/neon_dashboard/LICENSE b/packages/neon/neon_dashboard/LICENSE new file mode 120000 index 00000000..5853aaea --- /dev/null +++ b/packages/neon/neon_dashboard/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/packages/neon/neon_dashboard/analysis_options.yaml b/packages/neon/neon_dashboard/analysis_options.yaml new file mode 100644 index 00000000..66de1efd --- /dev/null +++ b/packages/neon/neon_dashboard/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:neon_lints/flutter.yaml + +analyzer: + exclude: + - lib/l10n/** diff --git a/packages/neon/neon_dashboard/assets/app.svg.vec b/packages/neon/neon_dashboard/assets/app.svg.vec new file mode 100644 index 0000000000000000000000000000000000000000..13983ab79a807b00b7d80f9373903258fc67acd6 GIT binary patch literal 292 zcmYe&?ORfcNPPPL{|+1X=Qu23RC6rZk>jx8qKjkjpUn;r&bt83 zW^e@YZvf>$@=t*589?=-7hD`){od>V;xFEw;{cL>xGx9jJ_ax!q#q;?G7qGF6VQAE KIiO2`VF3VfI%V7d literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/build.yaml b/packages/neon/neon_dashboard/build.yaml new file mode 100644 index 00000000..e69de29b diff --git a/packages/neon/neon_dashboard/l10n.yaml b/packages/neon/neon_dashboard/l10n.yaml new file mode 100644 index 00000000..7974745e --- /dev/null +++ b/packages/neon/neon_dashboard/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: lib/l10n +template-arb-file: en.arb +output-localization-file: localizations.dart +synthetic-package: false +output-dir: lib/l10n +nullable-getter: false diff --git a/packages/neon/neon_dashboard/lib/l10n/en.arb b/packages/neon/neon_dashboard/lib/l10n/en.arb new file mode 100644 index 00000000..2e2110b8 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/l10n/en.arb @@ -0,0 +1,4 @@ +{ + "@@locale": "en", + "noEntries": "No entries" +} diff --git a/packages/neon/neon_dashboard/lib/l10n/localizations.dart b/packages/neon/neon_dashboard/lib/l10n/localizations.dart new file mode 100644 index 00000000..0a2e2f7e --- /dev/null +++ b/packages/neon/neon_dashboard/lib/l10n/localizations.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'localizations_en.dart'; + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [Locale('en')]; + + /// No description provided for @noEntries. + /// + /// In en, this message translates to: + /// **'No entries'** + String get noEntries; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + } + + throw FlutterError('AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/packages/neon/neon_dashboard/lib/l10n/localizations_en.dart b/packages/neon/neon_dashboard/lib/l10n/localizations_en.dart new file mode 100644 index 00000000..d4a69229 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/l10n/localizations_en.dart @@ -0,0 +1,9 @@ +import 'localizations.dart'; + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get noEntries => 'No entries'; +} diff --git a/packages/neon/neon_dashboard/lib/neon_dashboard.dart b/packages/neon/neon_dashboard/lib/neon_dashboard.dart new file mode 100644 index 00000000..76f2a5f3 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/neon_dashboard.dart @@ -0,0 +1 @@ +export 'src/app.dart'; diff --git a/packages/neon/neon_dashboard/lib/src/app.dart b/packages/neon/neon_dashboard/lib/src/app.dart new file mode 100644 index 00000000..b1958f67 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/app.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:neon/models.dart'; +import 'package:neon_dashboard/l10n/localizations.dart'; +import 'package:neon_dashboard/src/blocs/dashboard.dart'; +import 'package:neon_dashboard/src/options.dart'; +import 'package:neon_dashboard/src/pages/main.dart'; +import 'package:neon_dashboard/src/routes.dart'; +import 'package:nextcloud/core.dart' as core; +import 'package:nextcloud/nextcloud.dart'; + +/// Implementation of the server `dashboard` app. +class DashboardApp extends AppImplementation { + /// Creates a new Dashboard app implementation instance. + DashboardApp(); + + @override + final String id = AppIDs.dashboard; + + @override + final LocalizationsDelegate localizationsDelegate = AppLocalizations.delegate; + + @override + final List supportedLocales = AppLocalizations.supportedLocales; + + @override + late final DashboardAppSpecificOptions options = DashboardAppSpecificOptions(storage); + + @override + DashboardBloc buildBloc(final Account account) => DashboardBloc(account); + + @override + final Widget page = const DashboardMainPage(); + + @override + final RouteBase route = $dashboardAppRoute; + + @override + (bool?, String?) isSupported( + final Account account, + final core.OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities, + ) => + const (null, null); +} diff --git a/packages/neon/neon_dashboard/lib/src/blocs/dashboard.dart b/packages/neon/neon_dashboard/lib/src/blocs/dashboard.dart new file mode 100644 index 00000000..9ae518b4 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/blocs/dashboard.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:neon/blocs.dart'; +import 'package:neon/models.dart'; +import 'package:neon_dashboard/src/utils/find.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; +import 'package:rxdart/rxdart.dart'; + +/// Events for [DashboardBloc]. +abstract class DashboardBlocEvents {} + +/// States for [DashboardBloc]. +abstract class DashboardBlocStates { + /// Dashboard widgets that are displayed. + BehaviorSubject>> get widgets; +} + +/// Implements the business logic for fetching dashboard widgets and their items. +class DashboardBloc extends InteractiveBloc implements DashboardBlocEvents, DashboardBlocStates { + /// Creates a new Dashboard Bloc instance. + /// + /// Automatically starts fetching the widgets and their items and refreshes everything every 30 seconds. + DashboardBloc(this._account) { + unawaited(refresh()); + + _timer = TimerBloc().registerTimer(const Duration(seconds: 30), refresh); + } + + final Account _account; + late final NeonTimer _timer; + + @override + BehaviorSubject>> widgets = BehaviorSubject(); + + @override + void dispose() { + _timer.cancel(); + unawaited(widgets.close()); + super.dispose(); + } + + @override + Future refresh() async { + widgets.add(widgets.valueOrNull?.asLoading() ?? Result.loading()); + + try { + final widgets = {}; + final v1WidgetIDs = []; + final v2WidgetIDs = []; + + final response = await _account.client.dashboard.dashboardApi.getWidgets(); + + for (final widget in response.body.ocs.data.values) { + if (widget.itemApiVersions.contains(2)) { + v2WidgetIDs.add(widget.id); + } else if (widget.itemApiVersions.contains(1)) { + v1WidgetIDs.add(widget.id); + } else { + debugPrint('Widget supports none of the API versions: ${widget.id}'); + } + } + + if (v1WidgetIDs.isNotEmpty) { + debugPrint('Loading v1 widgets: ${v1WidgetIDs.join(', ')}'); + + final response = await _account.client.dashboard.dashboardApi.getWidgetItems(widgets: v1WidgetIDs); + for (final entry in response.body.ocs.data.entries) { + widgets[entry.key] = dashboard.WidgetItems( + (final b) => b + ..items.replace(entry.value) + ..emptyContentMessage = '' + ..halfEmptyContentMessage = '', + ); + } + } + + if (v2WidgetIDs.isNotEmpty) { + debugPrint('Loading v2 widgets: ${v2WidgetIDs.join(', ')}'); + + final response = await _account.client.dashboard.dashboardApi.getWidgetItemsV2(widgets: v2WidgetIDs); + widgets.addEntries(response.body.ocs.data.entries); + } + + this.widgets.add( + Result.success( + widgets.map( + (final id, final items) => MapEntry( + response.body.ocs.data.values.find(id), + items, + ), + ), + ), + ); + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + + widgets.add(Result.error(e)); + return; + } + } +} diff --git a/packages/neon/neon_dashboard/lib/src/options.dart b/packages/neon/neon_dashboard/lib/src/options.dart new file mode 100644 index 00000000..56264300 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/options.dart @@ -0,0 +1,10 @@ +import 'package:neon/settings.dart'; + +/// Settings options specific to the dashboard app. +class DashboardAppSpecificOptions extends NextcloudAppOptions { + /// Creates a new dashboard options instance. + DashboardAppSpecificOptions(super.storage) { + super.categories = []; + super.options = []; + } +} diff --git a/packages/neon/neon_dashboard/lib/src/pages/main.dart b/packages/neon/neon_dashboard/lib/src/pages/main.dart new file mode 100644 index 00000000..2a19b6ad --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/pages/main.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:neon/blocs.dart'; +import 'package:neon/utils.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_dashboard/src/blocs/dashboard.dart'; +import 'package:neon_dashboard/src/widgets/widget.dart'; + +/// Displays the whole dashboard page layout. +class DashboardMainPage extends StatelessWidget { + /// Creates a new dashboard main page. + const DashboardMainPage({ + super.key, + }); + + @override + Widget build(final BuildContext context) { + final bloc = NeonProvider.of(context); + + return ResultBuilder.behaviorSubject( + stream: bloc.widgets, + builder: (final context, final snapshot) { + Widget? child; + if (snapshot.hasData) { + child = Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, + children: snapshot.requireData.entries + .map( + (final widget) => DashboardWidget( + widget: widget.key, + items: widget.value, + ), + ) + .toList(), + ); + } + + return Center( + child: NeonListView.custom( + isLoading: snapshot.isLoading, + error: snapshot.error, + onRefresh: bloc.refresh, + sliver: SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: child, + ), + ), + ), + ); + }, + ); + } +} diff --git a/packages/neon/neon_dashboard/lib/src/routes.dart b/packages/neon/neon_dashboard/lib/src/routes.dart new file mode 100644 index 00000000..8d3d080c --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/routes.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:neon/utils.dart'; +import 'package:neon_dashboard/src/pages/main.dart'; +import 'package:nextcloud/nextcloud.dart'; + +part 'routes.g.dart'; + +/// Route for the dashboard app. +@TypedGoRoute( + path: '$appsBaseRoutePrefix${AppIDs.dashboard}', + name: AppIDs.dashboard, +) +@immutable +class DashboardAppRoute extends NeonBaseAppRoute { + /// Creates a new dashboard app route. + const DashboardAppRoute(); + + @override + Widget build(final BuildContext context, final GoRouterState state) => const DashboardMainPage(); +} diff --git a/packages/neon/neon_dashboard/lib/src/routes.g.dart b/packages/neon/neon_dashboard/lib/src/routes.g.dart new file mode 100644 index 00000000..8dae6752 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/routes.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'routes.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [ + $dashboardAppRoute, + ]; + +RouteBase get $dashboardAppRoute => GoRouteData.$route( + path: '/apps/dashboard', + name: 'dashboard', + factory: $DashboardAppRouteExtension._fromState, + ); + +extension $DashboardAppRouteExtension on DashboardAppRoute { + static DashboardAppRoute _fromState(GoRouterState state) => const DashboardAppRoute(); + + String get location => GoRouteData.$location( + '/apps/dashboard', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} diff --git a/packages/neon/neon_dashboard/lib/src/utils/find.dart b/packages/neon/neon_dashboard/lib/src/utils/find.dart new file mode 100644 index 00000000..dccf391e --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/utils/find.dart @@ -0,0 +1,15 @@ +import 'package:collection/collection.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; + +/// Extension to find [dashboard.Widget]s. +extension WidgetFind on Iterable { + /// Finds the first widget that has the id set to [id]. + /// + /// Returns `null` if no matching widget was found. + dashboard.Widget? tryFind(final String id) => firstWhereOrNull((final widget) => widget.id == id); + + /// Finds the first widget that has the id set to [id]. + /// + /// Throws an exception if no matching widget was found. + dashboard.Widget find(final String id) => firstWhere((final widget) => widget.id == id); +} diff --git a/packages/neon/neon_dashboard/lib/src/widgets/widget.dart b/packages/neon/neon_dashboard/lib/src/widgets/widget.dart new file mode 100644 index 00000000..7e09e20b --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/widgets/widget.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:neon/theme.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_dashboard/l10n/localizations.dart'; +import 'package:neon_dashboard/src/widgets/widget_button.dart'; +import 'package:neon_dashboard/src/widgets/widget_item.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; + +/// Displays a single dashboard widget and its items. +class DashboardWidget extends StatelessWidget { + /// Creates a new dashboard widget items. + const DashboardWidget({ + required this.widget, + required this.items, + super.key, + }); + + /// The dashboard widget to be displayed. + final dashboard.Widget widget; + + /// The items of the widget to be displayed. + final dashboard.WidgetItems? items; + + @override + Widget build(final BuildContext context) { + final halfEmptyContentMessage = _renderMessage(items?.halfEmptyContentMessage); + final emptyContentMessage = _renderMessage(items?.emptyContentMessage); + + return SizedBox( + width: 320, + height: 560, + child: Card( + child: InkWell( + onTap: widget.widgetUrl != null && widget.widgetUrl!.isNotEmpty ? () => context.go(widget.widgetUrl!) : null, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: ListView( + padding: const EdgeInsets.all(8), + children: [ + ListTile( + title: Text( + widget.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + leading: SizedBox.square( + dimension: largeIconSize, + child: NeonUrlImage( + url: widget.iconUrl, + svgColorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + size: const Size.square(largeIconSize), + ), + ), + ), + const SizedBox( + height: 20, + ), + if (halfEmptyContentMessage != null) halfEmptyContentMessage, + if (emptyContentMessage != null) emptyContentMessage, + if (halfEmptyContentMessage == null && emptyContentMessage == null && (items?.items.isEmpty ?? true)) + _renderMessage(AppLocalizations.of(context).noEntries)!, + ...?items?.items.map( + (final item) => DashboardWidgetItem( + item: item, + roundIcon: widget.itemIconsRound, + ), + ), + const SizedBox( + height: 20, + ), + ...?widget.buttons?.map( + (final button) => DashboardWidgetButton( + button: button, + ), + ), + ], + ), + ), + ), + ); + } + + Widget? _renderMessage(final String? message) { + if (message == null || message.isEmpty) { + return null; + } + + return Center( + child: Column( + children: [ + const Icon( + Icons.check, + size: largeIconSize, + ), + Text(message), + ], + ), + ); + } +} diff --git a/packages/neon/neon_dashboard/lib/src/widgets/widget_button.dart b/packages/neon/neon_dashboard/lib/src/widgets/widget_button.dart new file mode 100644 index 00000000..4600d206 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/widgets/widget_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; + +/// Button inside a dashboard widget that is used to trigger an action. +class DashboardWidgetButton extends StatelessWidget { + /// Creates a new dashboard widget button. + const DashboardWidgetButton({ + required this.button, + super.key, + }); + + /// The dashboard widget button to be displayed. + final dashboard.Widget_Buttons button; + + @override + Widget build(final BuildContext context) { + void onPressed() => context.go(button.link); + final label = Text(button.text); + final icon = switch (button.type) { + 'new' => Icons.add, + 'more' => Icons.more_outlined, + 'setup' => Icons.launch, + _ => null, + }; + if (icon != null) { + return Align( + child: FilledButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: label, + ), + ); + } + + return Align( + child: FilledButton( + onPressed: onPressed, + child: label, + ), + ); + } +} diff --git a/packages/neon/neon_dashboard/lib/src/widgets/widget_item.dart b/packages/neon/neon_dashboard/lib/src/widgets/widget_item.dart new file mode 100644 index 00000000..d0502e90 --- /dev/null +++ b/packages/neon/neon_dashboard/lib/src/widgets/widget_item.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:neon/theme.dart'; +import 'package:neon/widgets.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; + +/// A single item in the dashboard widget. +class DashboardWidgetItem extends StatelessWidget { + /// Creates a new dashboard widget item. + const DashboardWidgetItem({ + required this.item, + required this.roundIcon, + super.key, + }); + + /// The dashboard widget item to be displayed. + final dashboard.WidgetItem item; + + /// Whether the leading icon should have round corners. + final bool roundIcon; + + @override + Widget build(final BuildContext context) { + Widget leading = SizedBox.square( + dimension: largeIconSize, + child: NeonImageWrapper( + borderRadius: roundIcon ? BorderRadius.circular(largeIconSize) : null, + child: NeonUrlImage( + url: item.iconUrl, + size: const Size.square(largeIconSize), + ), + ), + ); + if (item.overlayIconUrl.isNotEmpty) { + leading = Stack( + children: [ + leading, + SizedBox.square( + dimension: largeIconSize, + child: Align( + alignment: Alignment.bottomRight, + child: SizedBox.square( + dimension: smallIconSize, + child: NeonUrlImage( + url: item.overlayIconUrl, + size: const Size.square(smallIconSize), + ), + ), + ), + ), + ], + ); + } + return ListTile( + title: Text( + item.title, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + item.subtitle, + overflow: TextOverflow.ellipsis, + ), + leading: leading, + onTap: item.link.isNotEmpty ? () => context.go(item.link) : null, + ); + } +} diff --git a/packages/neon/neon_dashboard/pubspec.yaml b/packages/neon/neon_dashboard/pubspec.yaml new file mode 100644 index 00000000..ff86a386 --- /dev/null +++ b/packages/neon/neon_dashboard/pubspec.yaml @@ -0,0 +1,43 @@ +name: neon_dashboard +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: '>=3.1.0 <4.0.0' + flutter: '>=3.13.0' + +dependencies: + collection: ^1.0.0 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + go_router: ^12.0.0 + neon: + git: + url: https://github.com/nextcloud/neon + path: packages/neon/neon + nextcloud: + git: + url: https://github.com/nextcloud/neon + path: packages/nextcloud + rxdart: ^0.27.0 + +dev_dependencies: + build_runner: ^2.4.6 + built_collection: ^5.1.1 + flutter_test: + sdk: flutter + go_router_builder: ^2.3.3 + mocktail: ^1.0.1 + neon_lints: + git: + url: https://github.com/nextcloud/neon + path: packages/neon_lints + test: ^1.24.3 + vector_graphics_compiler: ^1.1.9 + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/packages/neon/neon_dashboard/pubspec_overrides.yaml b/packages/neon/neon_dashboard/pubspec_overrides.yaml new file mode 100644 index 00000000..e247d2d9 --- /dev/null +++ b/packages/neon/neon_dashboard/pubspec_overrides.yaml @@ -0,0 +1,12 @@ +# melos_managed_dependency_overrides: dynamite_runtime,neon,neon_lints,nextcloud,sort_box +dependency_overrides: + dynamite_runtime: + path: ../../dynamite/dynamite_runtime + neon: + path: ../neon + neon_lints: + path: ../../neon_lints + nextcloud: + path: ../../nextcloud + sort_box: + path: ../../sort_box diff --git a/packages/neon/neon_dashboard/test/find_test.dart b/packages/neon/neon_dashboard/test/find_test.dart new file mode 100644 index 00000000..4e046f94 --- /dev/null +++ b/packages/neon/neon_dashboard/test/find_test.dart @@ -0,0 +1,30 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:neon_dashboard/src/utils/find.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; +import 'package:test/test.dart'; + +// ignore: missing_override_of_must_be_overridden, avoid_implementing_value_types +class DashboardWidgetMock extends Mock implements dashboard.Widget {} + +void main() { + group('group name', () { + test('AccountFind', () { + final widget1 = DashboardWidgetMock(); + final widget2 = DashboardWidgetMock(); + + final widgets = { + widget1, + widget2, + }; + + when(() => widget1.id).thenReturn('widget1'); + when(() => widget2.id).thenReturn('widget2'); + + expect(widgets.tryFind('invalidID'), isNull); + expect(widgets.tryFind(widget2.id), equals(widget2)); + + expect(() => widgets.find('invalidID'), throwsA(isA())); + expect(widgets.find(widget2.id), equals(widget2)); + }); + }); +} diff --git a/packages/neon/neon_dashboard/test/goldens/widget.png b/packages/neon/neon_dashboard/test/goldens/widget.png new file mode 100644 index 0000000000000000000000000000000000000000..dd66e350fcef529f27bd3ef1548d3b1f37291800 GIT binary patch literal 4792 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 MboFyt=akR{04m%s5dZ)H literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/test/goldens/widget_button_more.png b/packages/neon/neon_dashboard/test/goldens/widget_button_more.png new file mode 100644 index 0000000000000000000000000000000000000000..e71445d9366a100c98650eceaa9dbd3f9b99b8e3 GIT binary patch literal 3006 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 literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/test/goldens/widget_not_round.png b/packages/neon/neon_dashboard/test/goldens/widget_not_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dd66e350fcef529f27bd3ef1548d3b1f37291800 GIT binary patch literal 4792 zcmeHLX;f2L621w61YDqD#DFMqr$q)t1u+C7g5by^VgP}VwBjHdQQEK=2#GL)w2a`^ zEucZ=C>n+YvBQ$cqBsy`i3?c-F|sR6CwAqIaZhISGesz(OPorn%9qiD@ANv zOT2fU4Ft z=iKQp!C}_e+8rX(*l&LaGW_mlMDr%%V5v@a96uup#ocBk6eRePE%tm2LZN>^6K z00-%9?&d%ei%inCCYXY&96T|)YOL+hNX1ZD`$1(jQ&3W3WNAQMzt+Y^+F6$loxu;^ zN?*cw)5jHxRNlaD*g;P$;)#ZZtSaraCU35e-C8+?#U#9|)nNsav`-nP02^ttC|sEY z6_-1hp{SQQgmNZVWieyQV^1bUMMZU<+mSS=gS9F2y}~_t^5p8G0mBdp5_Hq{u|G1E zj$mbnE(O6UK9oZqvb>Z*jNM-fQJ|(0C3yb zaNz4ci|VD@GM&tGy}i95VPO^tn&*XnK_uy!#dwfytL2f_HbLB)=?ymZ>TWKd*OmLKOpjFS(Y^>o6&s%mA(h@e-@Fq zUo2t^9TZLiM|gL@x`n4YvxDJK?0BrMb^B5 zSR7|c)|E{ZC1n^&_v6bAf7LJ;f+GZT+t*z=ytw9g0o0YzwoxgamV3+EwMR=$|1Odh zAOKB&5&xHo>i84wj~}gPPEEzj23%|6d7Mvg8BmPE{YbLZy{fzPHba|N2uW>CmKW2> zL9w034FSd&7g{@G(s4~HREtBd5abN=w5VwAfUVj`?y0n2?rv^`15Y?0$}1t0Pe~X1 z1W8&O`@;+i;r@6)lKy1To>2Zo*YYBjRJ+i!`^{h_m+w**$kMQi1bxwXK+OzBM7{2h z)J%wLE*E>UlB!i!G(-?(^B&4^_2rA%zszxXKXK{=yC+kBg_&w_C{_DA= ztDJVj-M8&nrAeHo&bljgj`N3t(OCGgxB&p{W(h)nqODT^yWVzsTHWA!=U71U2q%&A z;(|84b0#42`^Oo=7$qI@2OzE9z)KtuIWh5(H9p7Dc5uD6YS(H>#XMtxB_mh#j4DHl z7sQ9Z$6qD{sd~2-&Hll>oT{6|I$ky1`Q~(uYM^qmy4?S;Y$&vcIVBEZ4z4j=g96*m zFUV%wx|D|WcaZ8%)5pT33#<1pKmn{zAQ3tLio(S&_wLW7m3mW1wwC#9iUDd5lp^gT z8h({QpPr5B8PVsti?d*c=rHRk2hxY1q&*EEO zUWzYiUuKdTGYCL|ctK0{5c$$A!W@&>q_=9;5;07**#mbPI?;61!I6|2Ls-d^jS4&V zqKAlhmh$F>`Ku_E4#4_PKQw=L!j;2&@kka>LT>U6WFFAcp>^<{xUo@#x%syY!=0Wp z=9Xo^YXHAO>_|m`g6-^oBy|nE6(-bsthA-|dk5v;Ya2iVJ0Z$T{O|STspkBavL&fD z!*}IPGT&QgCBBR zW69E!{RDvXV$%=}zsVqLWq6T>6I>)1dS6S|+py6;c52a0XkUm*& zto*kSXKRrv%Ts=^HwlVTc-!U;4=|79at$d?0l!U5`mO&{wY6Y%i`ahTsc5MgG|vs5g!y!3q14YBJI2- zoc8$wjt;2rs0Wpr7W@d!9#d;aNQ~BmK~8}B(kD8BIrzT8k~Q)Ae(H#D@j&9e|1>k?a^bK_XEGlk&M-S3Z|KJ1*Hvno1;f lkV$?UIPe(aTM(-Crjcaeuh$bay(wf6kn5zH`3k zeV+Gu&v(8HULLNC^uEvo04&yfw^?F& zfW5$0ka}a>xrbH}x>i8gx~CI-vrc65Q+8(j>PP-y^|vj$q=klB!EFv3tUQ zscN@(D3Uck9dWE)e`9cim*Z2V>PTZk^+;pNxwj2=o=iu%65Eu(LQuU6V=u)fjrtma z97nM55iYBU0$gDh%a6s9uvz%yjtha8r3pIjWqePnqKn){B9Feh>$h*;c|)Bd6CWQR zY5SeS7+CHjyBfP}TwCxcyqa$ry1=O8`zadfz<-GETo1!LKk)=)J7)6-r8( zI^a>qgY-6@B%wdoeI8_dP1zyyh%N#@dJ- zfxs4&w(*w;doQ9tHlU9##0SWWXa=ezuU{X!q9FPCg}&hfpx>zXzM6RCKA`_FR)T&h zb%_Ijo3ZSVswt3vEGp^_j9?ma@_BDfi__bv62+`_xc^k^{WJf4^=B4}v@Y?=%Z;fu z1qB7Cxt$i#C@?A|LLX=n#|620JMqs}0(x%!?|F>b@2{XrFe|hJ+bcOYE62;{1MLr9 z;6F0#Y<7(7hYuf$)-cMD{Mt%q%4sw2#HEg^5M!ES^VQg)^rqluJBa=)_`k(xEu_$QM^BD+ zi`IDY&t>#PL%>hQ{dHuTAF)YSP(&vAk_V#O2}$-0fY`I_ zknL27!`<+{@d`eF%<@Z^r-{vDDw`TaJw>T6m5(k^B!su~d6n9&DrE(&a6TFv;O~YF;oFA9JDur$p3GtGmDDCoTVqN5b!|vFbmy zdHd|}3FnBQ!zEI4&s%M68saHN{@c%4e08e}b@00A_kb3 zfkU$0?GUHtWHEmt&(3@`gmi%)vCz`kgC)}ZG*v0n@oU^$PAp zINCJm(JoHAeZg?!*aO;uTUDz9j$`KmL34tETQeptyfyhT#vi89-b5$z`lyDEqxZNf znlWd7T12aDMCLF14Lx|KIw0`qE&5rWLb#6`M+qxu;yiGc8m$3x}@^{kDo>C^t z%6_XeSFh@~^qzV03>FUvg`_U{^_r9ttqlnWG_B&taxn1JSO-KN&GH9w(z>Zp0SMMFMQ@$xvZFmbVukcgW1?xY6~quDHz$mO#MevRR&CM{MpkinDSODMrEiV$`M}d3ignP%g+}DSgMahjs8mf_cB6uYTQkp#UkUDQv)DK}4S*Fz+fA^@ z*WKVVw8eR5Znu^X9!aau^A@yfS~!@U8-~@XdN>p$G&6ALnQDQ+^~iL-^1{?)nKk=y zV8$eJg{SIOsO7*aTidO+pL3m4AClw!rf#A!23E@0Nk|5WShIeRSI2Q-Cysc zv}+9_PX?FpCuUM916ipr+D#01g*fdsB4BE$mAGhejLE(tkhN~k5XH%=f$#aIn=Ack zltcNVja+8R@Ob4Q2RHRY;Z$##HMwA#mW?RZ%mf{IOh@t^wJN}}RpA*SlKKUNh^G8V zvhSZ5U>u0H_)ei}m#^kqBrJh4w2ZfZd8E#&z%X@ee4Virwvl{Mu|m~kNt*QaU~vD0r#Z{L)vS(F=N h8<4Y%|A%n6OVc9Ywirzs;Jjz??AY$HjpH16=I>HqXDt8# literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/test/goldens/widget_with_half_empty.png b/packages/neon/neon_dashboard/test/goldens/widget_with_half_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..9e65ba60b1f82b92c821cc3d7c99cc18853d4d2c GIT binary patch literal 4890 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 literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/test/goldens/widget_without_buttons.png b/packages/neon/neon_dashboard/test/goldens/widget_without_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..5239eddf56a12893d91c7c9ea0f0527489fe1961 GIT binary patch literal 3746 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 LcHUL8laTmtx0@Ul literal 0 HcmV?d00001 diff --git a/packages/neon/neon_dashboard/test/widget_test.dart b/packages/neon/neon_dashboard/test/widget_test.dart new file mode 100644 index 00000000..6ec81bca --- /dev/null +++ b/packages/neon/neon_dashboard/test/widget_test.dart @@ -0,0 +1,407 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon/blocs.dart'; +import 'package:neon/models.dart'; +import 'package:neon/theme.dart'; +import 'package:neon/utils.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_dashboard/l10n/localizations.dart'; +import 'package:neon_dashboard/src/widgets/widget.dart'; +import 'package:neon_dashboard/src/widgets/widget_button.dart'; +import 'package:neon_dashboard/src/widgets/widget_item.dart'; +import 'package:nextcloud/dashboard.dart' as dashboard; +import 'package:rxdart/rxdart.dart'; + +class MockAccountsBloc extends Mock implements AccountsBloc {} + +Widget wrapWidget(final AccountsBloc accountsBloc, final Widget child) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + backgroundColor: Colors.transparent, + body: NeonProvider.value( + value: accountsBloc, + child: child, + ), + ), + ); + +void main() { + final accountsBloc = MockAccountsBloc(); + when(() => accountsBloc.activeAccount).thenAnswer( + (final invocation) => BehaviorSubject.seeded( + Account( + serverURL: Uri(), + username: 'example', + ), + ), + ); + + group('Widget item', () { + final item = dashboard.WidgetItem( + (final b) => b + ..title = 'Widget item title' + ..subtitle = 'Widget item subtitle' + ..link = 'https://example.com/link' + ..iconUrl = 'https://example.com/iconUrl' + ..overlayIconUrl = 'https://example.com/overlayIconUrl' + ..sinceId = '', + ); + + testWidgets('Everything filled', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetItem( + item: item, + roundIcon: true, + ), + ), + ); + + expect(find.text('Widget item title'), findsOneWidget); + expect(find.text('Widget item subtitle'), findsOneWidget); + expect(find.byType(InkWell), findsOneWidget); + expect( + tester.widget(find.byType(InkWell)), + isA().having( + (final a) => a.onTap, + 'onTap is not null', + isNotNull, + ), + ); + expect(find.byType(NeonImageWrapper), findsOneWidget); + expect( + tester.widget(find.byType(NeonImageWrapper)), + isA().having( + (final a) => a.borderRadius, + 'borderRadius is correct', + BorderRadius.circular(largeIconSize), + ), + ); + expect(find.byType(NeonCachedImage), findsNWidgets(2)); + + expect(find.byType(DashboardWidgetItem), matchesGoldenFile('goldens/widget_item.png')); + }); + + testWidgets('Not round', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetItem( + item: item, + roundIcon: false, + ), + ), + ); + + expect( + tester.widget(find.byType(NeonImageWrapper)), + isA().having( + (final a) => a.borderRadius, + 'borderRadius is null', + null, + ), + ); + + expect(find.byType(DashboardWidgetItem), matchesGoldenFile('goldens/widget_item_not_round.png')); + }); + + testWidgets('Without link', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetItem( + item: item.rebuild((final b) => b..link = ''), + roundIcon: true, + ), + ), + ); + + expect( + tester.widget(find.byType(InkWell)), + isA().having( + (final a) => a.onTap, + 'onTap is null', + isNull, + ), + ); + }); + + testWidgets('Without overlayIconUrl', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetItem( + item: item.rebuild((final b) => b..overlayIconUrl = ''), + roundIcon: true, + ), + ), + ); + + expect(find.byType(NeonCachedImage), findsOneWidget); + }); + }); + + group('Widget button', () { + final button = dashboard.Widget_Buttons( + (final b) => b + ..type = 'new' + ..text = 'Button' + ..link = 'https://example.com/link', + ); + + testWidgets('New', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetButton( + button: button, + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('Button'), findsOneWidget); + + expect(find.byType(DashboardWidgetButton), matchesGoldenFile('goldens/widget_button_new.png')); + }); + + testWidgets('More', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetButton( + button: button.rebuild((final b) => b.type = 'more'), + ), + ), + ); + + expect(find.byIcon(Icons.more_outlined), findsOneWidget); + expect(find.text('Button'), findsOneWidget); + + expect(find.byType(DashboardWidgetButton), matchesGoldenFile('goldens/widget_button_more.png')); + }); + + testWidgets('Setup', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetButton( + button: button.rebuild((final b) => b.type = 'setup'), + ), + ), + ); + + expect(find.byIcon(Icons.launch), findsOneWidget); + expect(find.text('Button'), findsOneWidget); + + expect(find.byType(DashboardWidgetButton), matchesGoldenFile('goldens/widget_button_setup.png')); + }); + + testWidgets('Invalid', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidgetButton( + button: button.rebuild((final b) => b.type = 'test'), + ), + ), + ); + + expect(find.byType(Icon), findsNothing); + expect(find.text('Button'), findsOneWidget); + + expect(find.byType(DashboardWidgetButton), matchesGoldenFile('goldens/widget_button_invalid.png')); + }); + }); + + group('Widget', () { + final item = dashboard.WidgetItem( + (final b) => b + ..title = 'Widget item title' + ..subtitle = 'Widget item subtitle' + ..link = 'https://example.com/link' + ..iconUrl = 'https://example.com/iconUrl' + ..overlayIconUrl = 'https://example.com/overlayIconUrl' + ..sinceId = '', + ); + final items = dashboard.WidgetItems( + (final b) => b + ..items = BuiltList.from([item]).toBuilder() + ..emptyContentMessage = '' + ..halfEmptyContentMessage = '', + ); + final button = dashboard.Widget_Buttons( + (final b) => b + ..type = 'new' + ..text = 'Button' + ..link = 'https://example.com/link', + ); + final widget = dashboard.Widget( + (final b) => b + ..id = 'id' + ..title = 'Widget title' + ..order = 0 + ..iconClass = '' + ..iconUrl = 'https://example.com/iconUrl' + ..widgetUrl = 'https://example.com/widgetUrl' + ..itemIconsRound = true + ..itemApiVersions = BuiltList.from([1, 2]).toBuilder() + ..reloadInterval = 0 + ..buttons = BuiltList.from([button]).toBuilder(), + ); + + testWidgets('Everything filled', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget, + items: items, + ), + ), + ); + + expect(find.text('Widget title'), findsOneWidget); + expect(find.byType(InkWell), findsNWidgets(4)); + expect( + tester.widget(find.byType(InkWell).first), + isA().having( + (final a) => a.onTap, + 'onTap is not null', + isNotNull, + ), + ); + expect(find.byType(NeonImageWrapper), findsOneWidget); + expect( + tester.widget(find.byType(NeonImageWrapper)), + isA().having( + (final a) => a.borderRadius, + 'borderRadius is correct', + BorderRadius.circular(largeIconSize), + ), + ); + expect(find.byType(NeonCachedImage), findsNWidgets(3)); + expect(find.byType(DashboardWidgetItem), findsOneWidget); + expect(find.bySubtype(), findsOneWidget); + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('Button'), findsOneWidget); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget.png')); + }); + + testWidgets('Without widgetUrl', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget.rebuild((final b) => b.widgetUrl = ''), + items: items, + ), + ), + ); + + expect( + tester.widget(find.byType(InkWell).first), + isA().having( + (final a) => a.onTap, + 'onTap is null', + isNull, + ), + ); + }); + + testWidgets('Not round', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget.rebuild((final b) => b.itemIconsRound = false), + items: items, + ), + ), + ); + + expect( + tester.widget(find.byType(NeonImageWrapper)), + isA().having( + (final a) => a.borderRadius, + 'borderRadius is null', + null, + ), + ); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_not_round.png')); + }); + + testWidgets('With halfEmptyContentMessage', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget, + items: items.rebuild((final b) => b.halfEmptyContentMessage = 'Half empty'), + ), + ), + ); + + expect(find.text('Half empty'), findsOneWidget); + expect(find.byIcon(Icons.check), findsOneWidget); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_with_half_empty.png')); + }); + + testWidgets('With emptyContentMessage', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget, + items: items.rebuild((final b) => b.emptyContentMessage = 'Empty'), + ), + ), + ); + + expect(find.text('Empty'), findsOneWidget); + expect(find.byIcon(Icons.check), findsOneWidget); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_with_empty.png')); + }); + + testWidgets('Without items', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget, + items: null, + ), + ), + ); + + expect(find.text('No entries'), findsOneWidget); + expect(find.byIcon(Icons.check), findsOneWidget); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_without_items.png')); + }); + + testWidgets('Without buttons', (final tester) async { + await tester.pumpWidget( + wrapWidget( + accountsBloc, + DashboardWidget( + widget: widget.rebuild((final b) => b.buttons.clear()), + items: items, + ), + ), + ); + + expect(find.bySubtype(), findsNothing); + + expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_without_buttons.png')); + }); + }); +} From 1af20691dfcab5a07d39f4c0ceb7b9883d1f8749 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 1 Oct 2023 22:03:09 +0200 Subject: [PATCH 2/4] feat(tool,neon,app): Add dashboard Signed-off-by: jld3103 --- .../src/main/res/mipmap-hdpi/app_dashboard.png | Bin 0 -> 2261 bytes .../src/main/res/mipmap-mdpi/app_dashboard.png | Bin 0 -> 1483 bytes .../src/main/res/mipmap-xhdpi/app_dashboard.png | Bin 0 -> 2944 bytes .../src/main/res/mipmap-xxhdpi/app_dashboard.png | Bin 0 -> 4481 bytes .../main/res/mipmap-xxxhdpi/app_dashboard.png | Bin 0 -> 6154 bytes packages/app/lib/apps.dart | 2 ++ packages/app/pubspec.lock | 7 +++++++ packages/app/pubspec.yaml | 4 ++++ packages/app/pubspec_overrides.yaml | 4 +++- packages/neon/neon/lib/l10n/en.arb | 2 +- packages/neon/neon/lib/l10n/localizations.dart | 2 +- .../neon/neon/lib/l10n/localizations_en.dart | 1 + tool/generate-assets.sh | 1 + 13 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 packages/app/android/app/src/main/res/mipmap-hdpi/app_dashboard.png create mode 100644 packages/app/android/app/src/main/res/mipmap-mdpi/app_dashboard.png create mode 100644 packages/app/android/app/src/main/res/mipmap-xhdpi/app_dashboard.png create mode 100644 packages/app/android/app/src/main/res/mipmap-xxhdpi/app_dashboard.png create mode 100644 packages/app/android/app/src/main/res/mipmap-xxxhdpi/app_dashboard.png diff --git a/packages/app/android/app/src/main/res/mipmap-hdpi/app_dashboard.png b/packages/app/android/app/src/main/res/mipmap-hdpi/app_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..51ff0c82d8b431b735b85e0e4df74667a958a015 GIT binary patch literal 2261 zcmV;`2rBo9P)Nv|L2%1u2!?*&-SiNVS(3(xhW!FKJR+V>Pzc zvX@p%hZJR(M!%34YU78ZwSk1Rc&Vm^1uF%Hjh6&4Vrdt#BB8Os&dl!I&eIP&5!|`H zXU@zn$^Jf`^FGh}oPXZ)p7-*+M^u$*iepW708^c5fyyu~P#GE~t>#Eq!$@-EDx8LE zEY&td+8j|IfH=g}(wM58LhN^1ea*wH8O%&Zm{PA!`t*Z~+U%yLWg@x~xE16Yps7Oh z6!118yH(klA+zhE^+RWB7wtJ}D^NIW$L9@qsmc$5RY0Rho|7W{RpIwRU;NLkIhgXu zZyvP~D6-feAG7aKWi>F%`wGrOIgYR)n6iJzoveqjg z3*1&-Cl1>xx3%=|d*6+*tC95C?#}sH$KHDZhy!q)Bl`EV-JSE@7#p{s(TA5@p2=qS zi1JxC6;2}W+YP#;<=4^UCPqdr=uF=&7iH7gmoGqZ0ItYpc=7aui`q<#DxZr*#Im*H zIfNUGLo3Y~!V#1cz}eijR)kq7R|7ups*(kbzNXEH+(Ct}PQ?O^jJ9_R+~z*Ga|rB5 zbgQ5{{l1~MihhbCVkO2FFI2Tx0N+rwquRPv`C7au^iTkPQ&Fsy2$|^XXm?chP~A8u zt->EfWFYwHzQb-D#CkifSEO6uE~;CJNL%8B{Er=Yqg=uA0*y}}_EX&mc@RtPcPq?9 zV!a*L0~x%DBiKhP3gQf?qwwv&#{8NLVi;$#pa!ItPsufzceu8$rH_8(X-$WItw z3Nc0@6dMcOU9Mco7Ibp;!a%_G4Feauh~X?MtAhRe23^=zOuVOKg^FwgT3wi*45sWk zrB(q-wxFQZ@&jCoV<{_YEe=4SKf1$GR&eeMXu_qj#_ab?6)4_Bs zU3BDoSNL&oS z6sMx0dp!_4+iz@R>54+vi}XRd%*Z(s4X%$KG7YL$jJG0&%&ZDGihO}0T@5JLn)!CN zm<3Z8wi=edmT=gfcrj0)kz`^nhGPjqcNzv$b8J!5p+@tWdBb_@TzLYigXz-bsNXmA zmZ43}@gJT2I*>H6X%y>o6R+h7Br0Ybe!{z6Zq#|kmMvM}sELU>nm4V=7f3W{#3VpJN*d-mxBsxlPlgUpr<6ft@w>q~x%C|SBd zi_3q-EVEZeL@zffUuUi`DJ+uTIPwKjIb{@rGM|V{y;n8T)d0*hF%g`+XU~5yM~)bV z(vlckywK35<|M}Ai!hvUr$yeGcr8z$)r+{ zRAjZOO|4ML_88m1jzZT93Y3xTAchk?QNA@5T2Xv;=Zz?L8k&u!w6Wk?c0qx1N!rgF zR?+}$L_`ejydxr(Ht0r7_w7}-U)=v%!D&g+@rxqQm_-L5%i=wuhs^D~VPv$u8|4;L zJ4bYLv3o@Yn(@EPODIO!SOAJX8t>_-`)5Di*RfKNwJuEGYi;qrQtW2&5%8v=G{OcK z6*j8KwnR^8u?t&QB>FnqRb?Bl(sN{>)Ipijd-E;+r+^dYmD1`6gYn*wCka=ji1me* zJ1T#}aG55@hryKfbg2TR=TK_~#}M2!TY#X#3$eb?uWB+^6Onjt=Z{fd#$9FyDm+r| zd`I~Ns)$(e(a_#1)8AgjcJ2$VJ8-ZHA0HE6-Fc%n=tegk^2aE9gZ}8!a?8b)rQmgc z@e7XS?4!CAyo?IZiRBEq6)N7lWPyXzE$BC?E(I?oj)npQ(KpK#tT<0P-rM=J+zpMI_$S@w|d* zQbReCDDNVC0OU>)Y8%L9z*QJ7S(HU|dvN`MRi38-qpHbB=Uq*sx*Pbs+xQC5oXfpF zFaNqmgK_0xR+_(Ol7V7;6d}>G^fHz79);Pa@l7hqal27(uIW((0Du2$A7mZ*lEAR3 z?PMV^>{#}4Q*of`<52cXc>B85fj#dbRv^Xb&2qTTOTz?bbFfY&2GgR zcj4WU%mFGt2VZNZ#*xhPN%XZZQnj8`2zlbrJ^RGbdn*|PR)V+3!XskE#zJ?ax)!+9 zJI59Ap$d=8@I{}VlE=b7!2|P~hn-1j++A!9 z*=y=1P#)p1eP;H*W;a@LwW@WAiUc*6yd^3zK}Q7VRNB!aGl%C67oE`7n8_390%7W9 jWlS-q1uDa|KxOzp9|k@XGaUd!00000NkvXXu0mjf&|E|s literal 0 HcmV?d00001 diff --git a/packages/app/android/app/src/main/res/mipmap-mdpi/app_dashboard.png b/packages/app/android/app/src/main/res/mipmap-mdpi/app_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..4e66caa42bec49b4ccbe8cce3e7e08ae6c4337c1 GIT binary patch literal 1483 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 IoMq_$0bOz7L;wH) literal 0 HcmV?d00001 diff --git a/packages/app/lib/apps.dart b/packages/app/lib/apps.dart index ad1a2723..20731e47 100644 --- a/packages/app/lib/apps.dart +++ b/packages/app/lib/apps.dart @@ -1,4 +1,5 @@ import 'package:neon/models.dart'; +import 'package:neon_dashboard/neon_dashboard.dart'; import 'package:neon_files/neon_files.dart'; import 'package:neon_news/neon_news.dart'; import 'package:neon_notes/neon_notes.dart'; @@ -6,6 +7,7 @@ import 'package:neon_notifications/neon_notifications.dart'; /// The collection of clients enabled for the Neon app. final Set appImplementations = { + DashboardApp(), FilesApp(), NewsApp(), NotesApp(), diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 75980fe1..32e0e216 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -649,6 +649,13 @@ packages: relative: true source: path version: "1.0.0" + neon_dashboard: + dependency: "direct main" + description: + path: "../neon/neon_dashboard" + relative: true + source: path + version: "1.0.0" neon_files: dependency: "direct main" description: diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index 9c63b44a..0f014f51 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -13,6 +13,10 @@ dependencies: git: url: https://github.com/nextcloud/neon path: packages/neon/neon + neon_dashboard: + git: + url: https://github.com/nextcloud/neon + path: packages/neon/neon_dashboard neon_files: git: url: https://github.com/nextcloud/neon diff --git a/packages/app/pubspec_overrides.yaml b/packages/app/pubspec_overrides.yaml index 7bd861e4..e896bfc2 100644 --- a/packages/app/pubspec_overrides.yaml +++ b/packages/app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: dynamite_runtime,file_icons,neon,neon_files,neon_news,neon_notes,neon_notifications,nextcloud,sort_box,neon_lints +# melos_managed_dependency_overrides: dynamite_runtime,file_icons,neon,neon_files,neon_news,neon_notes,neon_notifications,nextcloud,sort_box,neon_lints,neon_dashboard dependency_overrides: dynamite_runtime: path: ../dynamite/dynamite_runtime @@ -6,6 +6,8 @@ dependency_overrides: path: ../file_icons neon: path: ../neon/neon + neon_dashboard: + path: ../neon/neon_dashboard neon_files: path: ../neon/neon_files neon_lints: diff --git a/packages/neon/neon/lib/l10n/en.arb b/packages/neon/neon/lib/l10n/en.arb index d7aa46cd..53515aa9 100644 --- a/packages/neon/neon/lib/l10n/en.arb +++ b/packages/neon/neon/lib/l10n/en.arb @@ -2,7 +2,7 @@ "@@locale": "en", "nextcloud": "Nextcloud", "nextcloudLogo": "Nextcloud logo", - "appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}", + "appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}", "@appImplementationName": { "placeholders": { "app": {} diff --git a/packages/neon/neon/lib/l10n/localizations.dart b/packages/neon/neon/lib/l10n/localizations.dart index d7f12d81..ce9f982a 100644 --- a/packages/neon/neon/lib/l10n/localizations.dart +++ b/packages/neon/neon/lib/l10n/localizations.dart @@ -104,7 +104,7 @@ abstract class NeonLocalizations { /// No description provided for @appImplementationName. /// /// In en, this message translates to: - /// **'{app, select, nextcloud{Nextcloud} core{Server} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}'** + /// **'{app, select, nextcloud{Nextcloud} core{Server} dashboard{Dashboard} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}'** String appImplementationName(String app); /// No description provided for @loginAgain. diff --git a/packages/neon/neon/lib/l10n/localizations_en.dart b/packages/neon/neon/lib/l10n/localizations_en.dart index c26c2ddf..e1aba5d6 100644 --- a/packages/neon/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/neon/lib/l10n/localizations_en.dart @@ -19,6 +19,7 @@ class NeonLocalizationsEn extends NeonLocalizations { { 'nextcloud': 'Nextcloud', 'core': 'Server', + 'dashboard': 'Dashboard', 'files': 'Files', 'news': 'News', 'notes': 'Notes', diff --git a/tool/generate-assets.sh b/tool/generate-assets.sh index 07d72075..0d910fe4 100755 --- a/tool/generate-assets.sh +++ b/tool/generate-assets.sh @@ -68,6 +68,7 @@ done precompile_assets ) +copy_app_svg dashboard external/nextcloud-server/apps/dashboard copy_app_svg files external/nextcloud-server/apps/files copy_app_svg news external/nextcloud-news copy_app_svg notes external/nextcloud-notes From a53f81f97c9e49a4b7c56890d4005421b6fa647a Mon Sep 17 00:00:00 2001 From: jld3103 Date: Mon, 2 Oct 2023 13:14:28 +0200 Subject: [PATCH 3/4] feat(neon): Define multiple fallback apps Signed-off-by: jld3103 --- packages/neon/neon/lib/src/blocs/apps.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/neon/neon/lib/src/blocs/apps.dart b/packages/neon/neon/lib/src/blocs/apps.dart index b3716a3a..411e1a77 100644 --- a/packages/neon/neon/lib/src/blocs/apps.dart +++ b/packages/neon/neon/lib/src/blocs/apps.dart @@ -97,9 +97,14 @@ class AppsBloc extends InteractiveBloc implements AppsBlocEvents, AppsBlocStates /// Returns null when no app is supported by the server. String? _getInitialAppFallback() { final supportedApps = appImplementations.value.requireData; - if (supportedApps.tryFind(AppIDs.files) != null) { - return AppIDs.files; - } else if (supportedApps.isNotEmpty) { + + for (final fallback in {AppIDs.dashboard, AppIDs.files}) { + if (supportedApps.tryFind(fallback) != null) { + return fallback; + } + } + + if (supportedApps.isNotEmpty) { return supportedApps.first.id; } From 17b5cc6d920ab1bef9bd2e4ed74731df6fc7b8ce Mon Sep 17 00:00:00 2001 From: jld3103 Date: Tue, 3 Oct 2023 10:37:49 +0200 Subject: [PATCH 4/4] chore: Update README.md Signed-off-by: jld3103 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bc73a34..c1735349 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ See [here](packages/app/README.md) for screenshots. | App | Status | |---------------------------------------------------|--------------------| +| [Dashboard](packages/neon/neon_dashboard) | :heavy_check_mark: | | [Files](packages/neon/neon_files) | :heavy_check_mark: | | [News](packages/neon/neon_news) | :heavy_check_mark: | | [Notes](packages/neon/neon_notes) | :heavy_check_mark: | @@ -57,7 +58,6 @@ See [here](packages/app/README.md) for screenshots. | Calendar | :rocket: | | Contacts | :rocket: | | Cookbook | :rocket: | -| Dashboard | :rocket: | | Photos | :rocket: | | Talk | :rocket: | | Tasks | :rocket: |