@ -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 |
@ -0,0 +1,5 @@ |
|||||||
|
include: package:neon_lints/flutter.yaml |
||||||
|
|
||||||
|
analyzer: |
||||||
|
exclude: |
||||||
|
- lib/l10n/** |
@ -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 |
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"@@locale": "en", |
||||||
|
"noEntries": "No entries" |
||||||
|
} |
@ -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<AppLocalizations>(context, AppLocalizations)!; |
||||||
|
} |
||||||
|
|
||||||
|
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ |
||||||
|
delegate, |
||||||
|
GlobalMaterialLocalizations.delegate, |
||||||
|
GlobalCupertinoLocalizations.delegate, |
||||||
|
GlobalWidgetsLocalizations.delegate, |
||||||
|
]; |
||||||
|
|
||||||
|
/// A list of this localizations delegate's supported locales. |
||||||
|
static const List<Locale> supportedLocales = <Locale>[Locale('en')]; |
||||||
|
|
||||||
|
/// No description provided for @noEntries. |
||||||
|
/// |
||||||
|
/// In en, this message translates to: |
||||||
|
/// **'No entries'** |
||||||
|
String get noEntries; |
||||||
|
} |
||||||
|
|
||||||
|
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { |
||||||
|
const _AppLocalizationsDelegate(); |
||||||
|
|
||||||
|
@override |
||||||
|
Future<AppLocalizations> load(Locale locale) { |
||||||
|
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale)); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
bool isSupported(Locale locale) => <String>['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.'); |
||||||
|
} |
@ -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'; |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export '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<DashboardBloc, DashboardAppSpecificOptions> { |
||||||
|
/// Creates a new Dashboard app implementation instance. |
||||||
|
DashboardApp(); |
||||||
|
|
||||||
|
@override |
||||||
|
final String id = AppIDs.dashboard; |
||||||
|
|
||||||
|
@override |
||||||
|
final LocalizationsDelegate<AppLocalizations> localizationsDelegate = AppLocalizations.delegate; |
||||||
|
|
||||||
|
@override |
||||||
|
final List<Locale> 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); |
||||||
|
} |
@ -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<Result<Map<dashboard.Widget, dashboard.WidgetItems?>>> 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<Result<Map<dashboard.Widget, dashboard.WidgetItems?>>> widgets = BehaviorSubject(); |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
_timer.cancel(); |
||||||
|
unawaited(widgets.close()); |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Future<void> refresh() async { |
||||||
|
widgets.add(widgets.valueOrNull?.asLoading() ?? Result.loading()); |
||||||
|
|
||||||
|
try { |
||||||
|
final widgets = <String, dashboard.WidgetItems?>{}; |
||||||
|
final v1WidgetIDs = <String>[]; |
||||||
|
final v2WidgetIDs = <String>[]; |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 = []; |
||||||
|
} |
||||||
|
} |
@ -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<DashboardBloc>(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, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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<DashboardAppRoute>( |
||||||
|
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(); |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND |
||||||
|
|
||||||
|
part of 'routes.dart'; |
||||||
|
|
||||||
|
// ************************************************************************** |
||||||
|
// GoRouterGenerator |
||||||
|
// ************************************************************************** |
||||||
|
|
||||||
|
List<RouteBase> 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<T?> push<T>(BuildContext context) => context.push<T>(location); |
||||||
|
|
||||||
|
void pushReplacement(BuildContext context) => context.pushReplacement(location); |
||||||
|
|
||||||
|
void replace(BuildContext context) => context.replace(location); |
||||||
|
} |
@ -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<dashboard.Widget> { |
||||||
|
/// 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); |
||||||
|
} |
@ -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), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -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/ |
@ -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 |
@ -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<StateError>())); |
||||||
|
expect(widgets.find(widget2.id), equals(widget2)); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
@ -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<AccountsBloc>.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<InkWell>().having( |
||||||
|
(final a) => a.onTap, |
||||||
|
'onTap is not null', |
||||||
|
isNotNull, |
||||||
|
), |
||||||
|
); |
||||||
|
expect(find.byType(NeonImageWrapper), findsOneWidget); |
||||||
|
expect( |
||||||
|
tester.widget(find.byType(NeonImageWrapper)), |
||||||
|
isA<NeonImageWrapper>().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<NeonImageWrapper>().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<InkWell>().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<dashboard.WidgetItem>.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<int>.from([1, 2]).toBuilder() |
||||||
|
..reloadInterval = 0 |
||||||
|
..buttons = BuiltList<dashboard.Widget_Buttons>.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<InkWell>().having( |
||||||
|
(final a) => a.onTap, |
||||||
|
'onTap is not null', |
||||||
|
isNotNull, |
||||||
|
), |
||||||
|
); |
||||||
|
expect(find.byType(NeonImageWrapper), findsOneWidget); |
||||||
|
expect( |
||||||
|
tester.widget(find.byType(NeonImageWrapper)), |
||||||
|
isA<NeonImageWrapper>().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<FilledButton>(), 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<InkWell>().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<NeonImageWrapper>().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<FilledButton>(), findsNothing); |
||||||
|
|
||||||
|
expect(find.byType(DashboardWidget), matchesGoldenFile('goldens/widget_without_buttons.png')); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |