From fd2a5183305ade3f2d4347c2fb185ec4b5cb72f6 Mon Sep 17 00:00:00 2001 From: Jonathan Gao Date: Mon, 18 Sep 2023 13:35:15 +0800 Subject: [PATCH] feat: Update Ansible Semaphore Client. --- lib/adaptive/alert_dialog.dart | 66 +++ lib/adaptive/app.dart | 59 +++ lib/adaptive/button.dart | 60 +++ lib/adaptive/checkbox.dart | 28 ++ lib/adaptive/dialog.dart | 28 ++ lib/adaptive/dropdown.dart | 104 +++++ lib/adaptive/floatingAction.dart | 35 ++ lib/adaptive/icon.dart | 64 +++ lib/adaptive/icon_button.dart | 27 ++ lib/adaptive/scaffold.dart | 66 +++ lib/adaptive/text_duration.dart | 33 ++ lib/adaptive/text_field.dart | 67 +++ lib/adaptive/text_null.dart | 29 ++ lib/adaptive/text_timeago.dart | 30 ++ lib/app.dart | 51 ++- lib/components/access_key/form.dart | 369 +++++++++-------- lib/components/access_key_name.dart | 19 +- lib/components/app_bar.dart | 8 +- lib/components/app_drawer.dart | 282 ++++++------- lib/components/environment/form.dart | 230 ++++++----- lib/components/environment_name.dart | 16 +- lib/components/inventory/form.dart | 319 +++++++-------- lib/components/inventory_name.dart | 16 +- lib/components/macos/sidebar.dart | 167 ++++++++ lib/components/macos/text_field.dart | 385 ++++++++++++++++++ lib/components/repository.dart | 19 +- lib/components/repository/form.dart | 251 ++++++------ lib/components/run_task_form.dart | 304 +++++++------- lib/components/task_output_view.dart | 98 +++-- lib/main.dart | 13 + lib/screens/home/home_screen.dart | 70 ++-- lib/screens/project/environment_screen.dart | 55 ++- lib/screens/project/history_screen.dart | 32 +- lib/screens/project/inventory_screen.dart | 55 ++- lib/screens/project/keystore_screen.dart | 55 ++- lib/screens/project/repository_screen.dart | 55 ++- lib/screens/project/team_screen.dart | 37 +- lib/screens/project/template_screen.dart | 38 +- lib/screens/project/template_task_screen.dart | 160 ++++---- lib/screens/sign_in/sign_in_screen.dart | 266 +++++------- lib/screens/splash/splash_screen.dart | 34 +- lib/screens/url_config/url_config_screen.dart | 96 ++--- lib/state/api_config.dart | 8 +- lib/state/projects.dart | 5 +- lib/state/projects/access_key.dart | 88 ++-- lib/state/projects/environment.dart | 100 +++-- lib/state/projects/inventory.dart | 105 ++--- lib/state/projects/repository.dart | 91 ++--- lib/state/projects/task.dart | 40 +- lib/state/projects/template.dart | 18 +- lib/state/projects/template_task.dart | 37 +- lib/utils/base_griddata.dart | 20 +- pubspec.lock | 99 ++++- pubspec.yaml | 9 +- 54 files changed, 3082 insertions(+), 1734 deletions(-) create mode 100644 lib/adaptive/alert_dialog.dart create mode 100644 lib/adaptive/app.dart create mode 100644 lib/adaptive/button.dart create mode 100644 lib/adaptive/checkbox.dart create mode 100644 lib/adaptive/dialog.dart create mode 100644 lib/adaptive/dropdown.dart create mode 100644 lib/adaptive/floatingAction.dart create mode 100644 lib/adaptive/icon.dart create mode 100644 lib/adaptive/icon_button.dart create mode 100644 lib/adaptive/scaffold.dart create mode 100644 lib/adaptive/text_duration.dart create mode 100644 lib/adaptive/text_field.dart create mode 100644 lib/adaptive/text_null.dart create mode 100644 lib/adaptive/text_timeago.dart create mode 100644 lib/components/macos/sidebar.dart create mode 100644 lib/components/macos/text_field.dart diff --git a/lib/adaptive/alert_dialog.dart b/lib/adaptive/alert_dialog.dart new file mode 100644 index 0000000..9395f73 --- /dev/null +++ b/lib/adaptive/alert_dialog.dart @@ -0,0 +1,66 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +import 'button.dart'; + +adaptiveAlertDialog({ + required BuildContext context, + required Widget title, + required Widget content, + required AdaptiveButton primaryButton, + AdaptiveButton? secondaryButton, +}) { + if (Platform.isMacOS) { + showMacosAlertDialog( + context: context, + builder: (context) => MacosAlertDialog( + appIcon: const SizedBox(), + title: title, + message: content, + //horizontalActions: false, + primaryButton: PushButton( + controlSize: ControlSize.large, + onPressed: primaryButton.onPressed, + child: primaryButton.child, + ), + secondaryButton: secondaryButton != null + ? PushButton( + secondary: true, + controlSize: ControlSize.large, + onPressed: secondaryButton.onPressed, + child: secondaryButton.child, + ) + : null, + ), + ); + } else { + showDialog( + context: context, + builder: (context) { + return AlertDialog.adaptive( + title: title, + content: content, + actions: secondaryButton == null + ? [ + TextButton( + onPressed: primaryButton.onPressed, + child: primaryButton.child, + ), + ] + : [ + TextButton( + onPressed: secondaryButton.onPressed, + child: secondaryButton.child, + ), + TextButton( + onPressed: primaryButton.onPressed, + child: primaryButton.child, + ), + ], + ); + }, + ); + } +} diff --git a/lib/adaptive/app.dart b/lib/adaptive/app.dart new file mode 100644 index 0000000..30355ff --- /dev/null +++ b/lib/adaptive/app.dart @@ -0,0 +1,59 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/constants.dart'; +import 'package:semaphore/state/theme.dart'; + +class AdaptiveApp extends StatelessWidget { + final bool debugShowCheckedModeBanner; + final String title; + final RouterConfig? routerConfig; + final ThemeMode themeMode; + final AppThemeData appThemeData; + + const AdaptiveApp({ + super.key, + this.title = Constants.appName, + this.debugShowCheckedModeBanner = false, + this.routerConfig, + this.themeMode = ThemeMode.system, + required this.appThemeData, + }); + + const AdaptiveApp.router({ + super.key, + this.title = Constants.appName, + this.debugShowCheckedModeBanner = false, + this.routerConfig, + this.themeMode = ThemeMode.system, + required this.appThemeData, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosApp.router( + localizationsDelegates: const >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + debugShowCheckedModeBanner: debugShowCheckedModeBanner, + routerConfig: routerConfig, + title: title, + themeMode: themeMode, + // theme: MacosThemeData.light(), + darkTheme: MacosThemeData.dark(), + ); + } + + return MaterialApp.router( + debugShowCheckedModeBanner: debugShowCheckedModeBanner, + routerConfig: routerConfig, + title: title, + themeMode: themeMode, + theme: appThemeData.lightThemeData, + darkTheme: appThemeData.darkThemeData, + ); + } +} diff --git a/lib/adaptive/button.dart b/lib/adaptive/button.dart new file mode 100644 index 0000000..7a91739 --- /dev/null +++ b/lib/adaptive/button.dart @@ -0,0 +1,60 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:material_neumorphic/material_neumorphic.dart' + show NeumorphicButton; + +class AdaptiveButton extends StatelessWidget { + final Function()? onPressed; + final Widget child; + final ControlSize controlSize; + + const AdaptiveButton({ + super.key, + this.onPressed, + this.child = const SizedBox(), + this.controlSize = ControlSize.regular, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return PushButton( + controlSize: controlSize, onPressed: onPressed, child: child); + } + return NeumorphicButton( + onPressed: onPressed, + child: child, + ); + } +} + +class AdaptiveTextButton extends StatelessWidget { + final Function()? onPressed; + final Widget child; + final ControlSize controlSize; + + const AdaptiveTextButton({ + super.key, + this.onPressed, + this.child = const SizedBox(), + this.controlSize = ControlSize.regular, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return PushButton( + secondary: true, + controlSize: controlSize, + onPressed: onPressed, + child: child); + } + return TextButton( + onPressed: onPressed, + child: child, + ); + } +} diff --git a/lib/adaptive/checkbox.dart b/lib/adaptive/checkbox.dart new file mode 100644 index 0000000..db00dbb --- /dev/null +++ b/lib/adaptive/checkbox.dart @@ -0,0 +1,28 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:material_neumorphic/material_neumorphic.dart'; + +class AdaptiveCheckbox extends StatelessWidget { + final void Function(bool)? onChanged; + final bool? value; + + const AdaptiveCheckbox({ + super.key, + this.onChanged, + this.value, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosCheckbox(onChanged: onChanged, value: value); + } + return NeumorphicCheckbox( + value: value ?? false, + onChanged: onChanged ?? (value) {}, + ); + } +} diff --git a/lib/adaptive/dialog.dart b/lib/adaptive/dialog.dart new file mode 100644 index 0000000..6a8e387 --- /dev/null +++ b/lib/adaptive/dialog.dart @@ -0,0 +1,28 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +adaptiveDialog({ + required BuildContext context, + required Widget child, +}) { + if (Platform.isMacOS) { + showMacosSheet( + context: context, + barrierDismissible: true, + builder: (_) => MacosSheet( + child: child, + ), + ); + } else { + showDialog( + context: context, + builder: (context) { + return Dialog.fullscreen( + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + child: child, + ); + }); + } +} diff --git a/lib/adaptive/dropdown.dart b/lib/adaptive/dropdown.dart new file mode 100644 index 0000000..1da49dc --- /dev/null +++ b/lib/adaptive/dropdown.dart @@ -0,0 +1,104 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveDropdownMenu extends StatelessWidget { + final T? value; + final void Function(T?)? onChanged; + final InputDecoration? decoration; + final List>? items; + + const AdaptiveDropdownMenu({ + super.key, + this.value, + this.onChanged, + this.decoration, + this.items, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + decoration?.labelText != null + ? Text(decoration!.labelText!, style: theme.typography.headline) + : Container(), + MacosPopupButton( + key: key, + value: value, + onChanged: onChanged, + items: items + ?.map>((item) => MacosPopupMenuItem( + key: item.key, + onTap: item.onTap, + enabled: item.enabled, + alignment: item.alignment, + value: item.value, + child: item.child, + )) + .toList(), + ), + ], + ); + } + return DropdownButtonFormField( + key: key, + decoration: decoration, + onChanged: onChanged, + value: value, + items: items + ?.map>((item) => DropdownMenuItem( + key: item.key, + onTap: item.onTap, + enabled: item.enabled, + alignment: item.alignment, + value: item.value, + child: item.child, + )) + .toList(), + ); + } +} + +class AdaptiveDropdownMenuItem extends StatelessWidget { + final T? value; + final Widget child; + final void Function()? onTap; + final bool enabled; + final AlignmentGeometry alignment; + + const AdaptiveDropdownMenuItem({ + super.key, + this.enabled = true, + this.onTap, + this.alignment = AlignmentDirectional.centerStart, + this.value, + required this.child, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosPopupMenuItem( + key: key, + onTap: onTap, + enabled: enabled, + alignment: alignment, + value: value, + child: child, + ); + } + return DropdownMenuItem( + key: key, + onTap: onTap, + enabled: enabled, + alignment: alignment, + value: value, + child: child, + ); + } +} diff --git a/lib/adaptive/floatingAction.dart b/lib/adaptive/floatingAction.dart new file mode 100644 index 0000000..4f57bff --- /dev/null +++ b/lib/adaptive/floatingAction.dart @@ -0,0 +1,35 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:material_neumorphic/material_neumorphic.dart' + show NeumorphicFloatingActionButton; + +class AdaptiveFloatingAction { + final Function()? onPressed; + final Widget icon; + final String label; + + const AdaptiveFloatingAction({ + this.onPressed, + this.icon = const SizedBox(), + this.label = '', + }); + + ToolbarItem toolBarIconButton() { + return ToolBarIconButton( + icon: icon, + onPressed: onPressed, + showLabel: false, + label: label, + ); + } + + Widget floatingActionButton() { + return NeumorphicFloatingActionButton( + onPressed: onPressed, + child: icon, + ); + } +} diff --git a/lib/adaptive/icon.dart b/lib/adaptive/icon.dart new file mode 100644 index 0000000..8b7391a --- /dev/null +++ b/lib/adaptive/icon.dart @@ -0,0 +1,64 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveIcon extends StatelessWidget { + final IconData? icon; + final double? size; + final double? fill; + final double? weight; + final double? grade; + final double? opticalSize; + final Color? color; + final List? shadows; + final String? semanticLabel; + final TextDirection? textDirection; + + const AdaptiveIcon( + this.icon, { + super.key, + this.size, + this.fill, + this.weight, + this.grade, + this.opticalSize, + this.color, + this.shadows, + this.semanticLabel, + this.textDirection, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + Color? color_ = color; + if (color_ == null) { + final theme = MacosTheme.of(context); + color_ = theme.typography.body.color; + } + return MacosIcon( + icon, + key: key, + size: size, + color: color_, + semanticLabel: semanticLabel, + textDirection: textDirection, + ); + } + return Icon( + icon, + key: key, + size: size, + fill: fill, + weight: weight, + grade: grade, + opticalSize: opticalSize, + color: color, + shadows: shadows, + semanticLabel: semanticLabel, + textDirection: textDirection, + ); + } +} diff --git a/lib/adaptive/icon_button.dart b/lib/adaptive/icon_button.dart new file mode 100644 index 0000000..3bc8bc9 --- /dev/null +++ b/lib/adaptive/icon_button.dart @@ -0,0 +1,27 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveIconButton extends StatelessWidget { + final Function()? onPressed; + final IconData? iconData; + + const AdaptiveIconButton({ + super.key, + this.onPressed, + this.iconData, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosIconButton(onPressed: onPressed, icon: MacosIcon(iconData)); + } + return IconButton( + onPressed: onPressed, + icon: Icon(iconData), + ); + } +} diff --git a/lib/adaptive/scaffold.dart b/lib/adaptive/scaffold.dart new file mode 100644 index 0000000..b2efce1 --- /dev/null +++ b/lib/adaptive/scaffold.dart @@ -0,0 +1,66 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/components/app_bar.dart'; +import 'package:semaphore/components/macos/sidebar.dart'; + +class AdaptiveScaffold extends ConsumerWidget { + final Widget? drawer; + final LocalAppBar? appBar; + final Widget? body; + final AdaptiveFloatingAction? floatingAction; + final Widget? floatingActionButton; + final Widget Function(BuildContext, ScrollController)? bodyBuilder; + + const AdaptiveScaffold({ + super.key, + this.drawer, + this.appBar, + this.body, + this.floatingAction, + this.floatingActionButton, + this.bodyBuilder, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (Platform.isMacOS) { + final toolbarAction = floatingAction?.toolBarIconButton(); + return MacosWindow( + titleBar: appBar?.title != null + ? TitleBar(title: Text(appBar!.title)) + : null, + sidebar: buildSidebar(context), + child: MacosScaffold( + backgroundColor: const Color.fromRGBO(0xff, 0xff, 0xff, 0), + toolBar: toolbarAction != null + ? ToolBar(actions: [ + toolbarAction, + ]) + : null, + children: [ + ContentArea(builder: (context, scrollController) { + if (bodyBuilder != null) { + return bodyBuilder!(context, scrollController); + } + return Material( + color: MacosTheme.of(context).canvasColor, + child: body ?? Container()); + }), + ], + )); + } + return Scaffold( + drawer: drawer, + appBar: appBar, + body: body, + floatingActionButton: floatingAction != null + ? floatingAction!.floatingActionButton() + : floatingActionButton, + ); + } +} diff --git a/lib/adaptive/text_duration.dart b/lib/adaptive/text_duration.dart new file mode 100644 index 0000000..6f98d6e --- /dev/null +++ b/lib/adaptive/text_duration.dart @@ -0,0 +1,33 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveTextDuration extends StatelessWidget { + final DateTime? start; + final DateTime? end; + + const AdaptiveTextDuration(this.start, this.end, {super.key}); + + @override + Widget build(BuildContext context) { + final DateTime endTime = end ?? DateTime.now(); + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + if (start == null) { + return Text('--', style: theme.typography.body); + } + final DateTime startTime = start!; + final d = endTime.difference(startTime); + return Text('${d.inSeconds} ${d.inSeconds > 1 ? "seconds" : "second"}', + style: theme.typography.body); + } + if (start == null) { + return const Text('--'); + } + final DateTime startTime = start!; + final d = endTime.difference(startTime); + return Text('${d.inSeconds} ${d.inSeconds > 1 ? "seconds" : "second"}'); + } +} diff --git a/lib/adaptive/text_field.dart b/lib/adaptive/text_field.dart new file mode 100644 index 0000000..a5505ae --- /dev/null +++ b/lib/adaptive/text_field.dart @@ -0,0 +1,67 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/components/macos/text_field.dart'; + +class AdaptiveTextField extends StatelessWidget { + final String? initialValue; + final void Function(String)? onChanged; + final InputDecoration? decoration; + final bool obscureText; + final bool enableSuggestions; + final bool autocorrect; + final Iterable? autofillHints; + final int? minLines; + final int? maxLines; + + const AdaptiveTextField({ + super.key, + this.initialValue, + this.onChanged, + this.decoration, + this.obscureText = false, + this.enableSuggestions = false, + this.autocorrect = false, + this.autofillHints, + this.minLines, + this.maxLines = 1, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + decoration?.labelText != null + ? Text(decoration!.labelText!, style: theme.typography.headline) + : Container(), + MacosTextFormField( + initialValue: initialValue, + obscureText: obscureText, + enableSuggestions: enableSuggestions, + autocorrect: autocorrect, + autofillHints: autofillHints, + onChanged: onChanged, + minLines: minLines, + maxLines: maxLines, + ), + ], + ); + } + return TextFormField( + initialValue: initialValue, + decoration: decoration, + obscureText: obscureText, + enableSuggestions: enableSuggestions, + autocorrect: autocorrect, + autofillHints: autofillHints, + onChanged: onChanged, + minLines: minLines, + maxLines: maxLines, + ); + } +} diff --git a/lib/adaptive/text_null.dart b/lib/adaptive/text_null.dart new file mode 100644 index 0000000..ce3dfcb --- /dev/null +++ b/lib/adaptive/text_null.dart @@ -0,0 +1,29 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveTextNull extends StatelessWidget { + final String? data; + + const AdaptiveTextNull( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + if (data == 'null') { + return Text('--', style: theme.typography.body); + } + return Text(data ?? '--', style: theme.typography.body); + } + if (data == 'null') { + return const Text('--'); + } + return Text(data ?? '--'); + } +} diff --git a/lib/adaptive/text_timeago.dart b/lib/adaptive/text_timeago.dart new file mode 100644 index 0000000..80ab680 --- /dev/null +++ b/lib/adaptive/text_timeago.dart @@ -0,0 +1,30 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class AdaptiveTextTimeago extends StatelessWidget { + final DateTime? data; + + const AdaptiveTextTimeago( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + if (data == null) { + return Text('--', style: theme.typography.body); + } + return Text(timeago.format(data!), style: theme.typography.body); + } + if (data == null) { + return const Text('--'); + } + return Text(timeago.format(data!)); + } +} diff --git a/lib/app.dart b/lib/app.dart index e2b37a2..2f76e81 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,8 +1,13 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:semaphore/constants.dart'; import 'package:semaphore/router/router.dart'; import 'package:semaphore/state/theme.dart'; +import 'package:semaphore/adaptive/app.dart'; +import 'package:system_theme/system_theme.dart'; +import 'package:system_tray/system_tray.dart'; class App extends ConsumerStatefulWidget { const App({Key? key}) : super(key: key); @@ -15,6 +20,9 @@ class _AppState extends ConsumerState { @override initState() { super.initState(); + if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + initSystemTray(); + } } @override @@ -22,19 +30,54 @@ class _AppState extends ConsumerState { super.dispose(); } + Future initSystemTray() async { + String path = + Platform.isWindows ? 'assets/icon/icon.png' : 'assets/icon/icon.png'; + + final AppWindow appWindow = AppWindow(); + final SystemTray systemTray = SystemTray(); + + // We first init the systray menu + await systemTray.initSystemTray( + toolTip: 'Ansible Semaphore Client', + iconPath: path, + ); + + // create context menu + final Menu menu = Menu(); + await menu.buildFrom([ + MenuItemLabel(label: 'Show', onClicked: (menuItem) => appWindow.show()), + MenuItemLabel(label: 'Hide', onClicked: (menuItem) => appWindow.hide()), + MenuItemLabel(label: 'Exit', onClicked: (menuItem) => appWindow.close()), + ]); + + // set context menu + await systemTray.setContextMenu(menu); + + // handle system tray event + systemTray.registerSystemTrayEventHandler((eventName) { + debugPrint('eventName: $eventName'); + if (eventName == kSystemTrayEventClick) { + Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu(); + } else if (eventName == kSystemTrayEventRightClick) { + Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show(); + } + }); + } + @override Widget build(BuildContext context) { final router = ref.watch(routerProvider); final themeMode = ref.watch(themeModeProvider); final appThemeData = ref.watch(localAppThemeDataProvider); + final accentColor = SystemTheme.accentColor.accent; - return MaterialApp.router( + return AdaptiveApp.router( debugShowCheckedModeBanner: false, routerConfig: router, title: Constants.appName, - themeMode: themeMode, - theme: appThemeData.lightThemeData, - darkTheme: appThemeData.darkThemeData, + // themeMode: ThemeMode.system, + appThemeData: appThemeData, ); } } diff --git a/lib/components/access_key/form.dart b/lib/components/access_key/form.dart index ae3d8d2..3c3e0b2 100644 --- a/lib/components/access_key/form.dart +++ b/lib/components/access_key/form.dart @@ -1,7 +1,13 @@ +import 'dart:io'; + import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dropdown.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/access_key.dart'; class AccessKeyForm extends ConsumerWidget { @@ -11,197 +17,198 @@ class AccessKeyForm extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); final accessKey = ref.watch(accessKeyFamily(accessKeyId)); final formData = ref.watch(accessKeyFormRequestProvider(accessKey.value)); + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + return accessKey.when( data: (accessKey) { return SingleChildScrollView( - child: Form( - child: Column(children: [ - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(24), - child: Text( - accessKey.id == null ? 'New AccessKey' : 'Edit AccessKey', - style: theme.textTheme.titleLarge), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.name, - onChanged: (value) { - ref - .read(accessKeyFormRequestProvider(accessKey).notifier) - .updateWith(name: value); - }, - decoration: const InputDecoration( - labelText: 'Key Name', contentPadding: EdgeInsets.all(8)), - ), - NeumorphicDropdownButtonFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - isExpanded: true, - dropdownColor: theme.colorScheme.secondaryContainer, - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.onSecondaryContainer), - decoration: const InputDecoration( - labelText: 'Type', contentPadding: EdgeInsets.all(8)), - value: formData.type, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onSecondaryContainer), - onChanged: (AccessKeyRequestTypeEnum? value) { - ref - .read(accessKeyFormRequestProvider(accessKey).notifier) - .updateWith(type: value); - }, - items: AccessKeyRequestTypeEnum.values - .map>( - (AccessKeyRequestTypeEnum value) { - return DropdownMenuItem( - value: value, - child: Text(value.name, - style: theme.textTheme.titleMedium!.copyWith( - color: theme.colorScheme.onSecondaryContainer)), - ); - }).toList(), - ), - formData.type == AccessKeyRequestTypeEnum.ssh - ? Column( - children: [ - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: - const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.ssh?.login, - onChanged: (value) { - final AccessKeyRequestSsh ssh = - formData.ssh ?? AccessKeyRequestSsh(); - ref - .read(accessKeyFormRequestProvider(accessKey) - .notifier) - .updateWith( - ssh: AccessKeyRequestSsh( - login: value, - privateKey: ssh.privateKey)); - }, - decoration: const InputDecoration( - labelText: 'Username (Optional)', - contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: - const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.ssh?.privateKey, - onChanged: (value) { - final AccessKeyRequestSsh ssh = - formData.ssh ?? AccessKeyRequestSsh(); - ref - .read(accessKeyFormRequestProvider(accessKey) - .notifier) - .updateWith( - ssh: AccessKeyRequestSsh( - login: ssh.login, privateKey: value)); - }, - minLines: 5, - maxLines: null, - decoration: const InputDecoration( - labelText: 'Private Key', - alignLabelWithHint: true, - contentPadding: EdgeInsets.all(8)), - ), - ], - ) - : Container(), - formData.type == AccessKeyRequestTypeEnum.loginPassword - ? Column( - children: [ - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: - const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.loginPassword?.login, - onChanged: (value) { - final AccessKeyRequestLoginPassword loginPassword = - formData.loginPassword ?? - AccessKeyRequestLoginPassword(); - ref - .read(accessKeyFormRequestProvider(accessKey) - .notifier) - .updateWith( - loginPassword: - AccessKeyRequestLoginPassword( - login: value, - password: loginPassword.password)); - }, - decoration: const InputDecoration( - labelText: 'Login (Optional)', - contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: - const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.ssh?.login, - onChanged: (value) { - final AccessKeyRequestLoginPassword loginPassword = - formData.loginPassword ?? - AccessKeyRequestLoginPassword(); - ref - .read(accessKeyFormRequestProvider(accessKey) - .notifier) - .updateWith( - loginPassword: - AccessKeyRequestLoginPassword( - login: loginPassword.login, - password: value)); - }, - decoration: const InputDecoration( - labelText: 'Password', - contentPadding: EdgeInsets.all(8)), - ), - ], - ) - : Container(), - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - alignment: Alignment.bottomRight, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(24), + child: Text( + accessKey.id == null + ? 'New AccessKey' + : 'Edit AccessKey', + style: textTitleStyle), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { + ref + .read(accessKeyFormRequestProvider(accessKey) + .notifier) + .updateWith(name: value); }, - child: const Text('Cancel')), - const SizedBox( - width: 24, - ), - NeumorphicButton( - onPressed: () async { - await ref + decoration: const InputDecoration( + labelText: 'Key Name', + contentPadding: EdgeInsets.all(8)), + ), + AdaptiveDropdownMenu( + decoration: const InputDecoration( + labelText: 'Type', contentPadding: EdgeInsets.all(8)), + value: formData.type, + onChanged: (AccessKeyRequestTypeEnum? value) { + ref .read(accessKeyFormRequestProvider(accessKey) .notifier) - .postAccessKey(); - if (context.mounted) Navigator.of(context).pop(); - ref.read(accessKeyListProvider.notifier).loadRows(); + .updateWith(type: value); }, - child: const Text('Create')), - ]), - ), - ]), + items: AccessKeyRequestTypeEnum.values.map< + AdaptiveDropdownMenuItem< + AccessKeyRequestTypeEnum>>( + (AccessKeyRequestTypeEnum value) { + return AdaptiveDropdownMenuItem< + AccessKeyRequestTypeEnum>( + value: value, + child: Text(value.name), + ); + }).toList(), + ), + formData.type == AccessKeyRequestTypeEnum.ssh + ? Column( + children: [ + AdaptiveTextField( + autocorrect: false, + initialValue: formData.ssh?.login, + onChanged: (value) { + final AccessKeyRequestSsh ssh = + formData.ssh ?? AccessKeyRequestSsh(); + ref + .read(accessKeyFormRequestProvider( + accessKey) + .notifier) + .updateWith( + ssh: AccessKeyRequestSsh( + login: value, + privateKey: ssh.privateKey)); + }, + decoration: const InputDecoration( + labelText: 'Username (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.ssh?.privateKey, + onChanged: (value) { + final AccessKeyRequestSsh ssh = + formData.ssh ?? AccessKeyRequestSsh(); + ref + .read(accessKeyFormRequestProvider( + accessKey) + .notifier) + .updateWith( + ssh: AccessKeyRequestSsh( + login: ssh.login, + privateKey: value)); + }, + minLines: 5, + maxLines: null, + decoration: const InputDecoration( + labelText: 'Private Key', + alignLabelWithHint: true, + contentPadding: EdgeInsets.all(8)), + ), + ], + ) + : Container(), + formData.type == AccessKeyRequestTypeEnum.loginPassword + ? Column( + children: [ + AdaptiveTextField( + autocorrect: false, + initialValue: formData.loginPassword?.login, + onChanged: (value) { + final AccessKeyRequestLoginPassword + loginPassword = formData.loginPassword ?? + AccessKeyRequestLoginPassword(); + ref + .read(accessKeyFormRequestProvider( + accessKey) + .notifier) + .updateWith( + loginPassword: + AccessKeyRequestLoginPassword( + login: value, + password: + loginPassword.password)); + }, + decoration: const InputDecoration( + labelText: 'Login (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.ssh?.login, + onChanged: (value) { + final AccessKeyRequestLoginPassword + loginPassword = formData.loginPassword ?? + AccessKeyRequestLoginPassword(); + ref + .read(accessKeyFormRequestProvider( + accessKey) + .notifier) + .updateWith( + loginPassword: + AccessKeyRequestLoginPassword( + login: loginPassword.login, + password: value)); + }, + decoration: const InputDecoration( + labelText: 'Password', + contentPadding: EdgeInsets.all(8)), + ), + ], + ) + : Container(), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.bottomRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveTextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + const SizedBox( + width: 24, + ), + AdaptiveButton( + onPressed: () async { + await ref + .read(accessKeyFormRequestProvider( + accessKey) + .notifier) + .postAccessKey(); + if (context.mounted) + Navigator.of(context).pop(); + ref + .read(accessKeyListProvider.notifier) + .loadRows(); + }, + child: const Text('Create')), + ]), + ), + ]), + ), ), ); }, diff --git a/lib/components/access_key_name.dart b/lib/components/access_key_name.dart index 0476667..801fbe3 100644 --- a/lib/components/access_key_name.dart +++ b/lib/components/access_key_name.dart @@ -1,5 +1,8 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:semaphore/state/projects/access_key.dart'; class AccessKeyName extends ConsumerWidget { @@ -12,11 +15,19 @@ class AccessKeyName extends ConsumerWidget { final accessKey = ref.watch(accessKeyFamily(id)); return accessKey.when( - data: (accessKey) => Text( - accessKey.name ?? '--', - ), + data: (accessKey) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Text(accessKey.name ?? '--', style: theme.typography.body); + } + return Text( + accessKey.name ?? '--', + ); + }, loading: () => const LinearProgressIndicator(), - error: (error, stackTrace) => const Text('N/A'), + error: (error, stackTrace) => Platform.isMacOS + ? Text('N/A', style: MacosTheme.of(context).typography.body) + : const Text('N/A'), ); } } diff --git a/lib/components/app_bar.dart b/lib/components/app_bar.dart index ff54324..98a1c9b 100644 --- a/lib/components/app_bar.dart +++ b/lib/components/app_bar.dart @@ -9,16 +9,16 @@ class LocalAppBar extends ConsumerWidget implements PreferredSizeWidget { @override final Size preferredSize; - const LocalAppBar({Key? key, String? title, this.actions = const []}) - : title = title ?? 'Ansible Semaphore', - preferredSize = const Size.fromHeight(NeumorphicAppBar.toolbarHeight), + const LocalAppBar( + {Key? key, this.title = 'Ansible Semaphore', this.actions = const []}) + : preferredSize = const Size.fromHeight(NeumorphicAppBar.toolbarHeight), super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final neumorphicTheme = theme.extension()!; - final style = neumorphicTheme.getNeumorphicStyle(); + final style = neumorphicTheme.style; return NeumorphicAppBar( depth: -4, diff --git a/lib/components/app_drawer.dart b/lib/components/app_drawer.dart index 2c24a9a..0eabd71 100644 --- a/lib/components/app_drawer.dart +++ b/lib/components/app_drawer.dart @@ -23,7 +23,6 @@ class LocalDrawer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; final themeMode = ref.watch(themeModeProvider); final router = ref.read(routerProvider); @@ -32,172 +31,127 @@ class LocalDrawer extends ConsumerWidget { final currentProject = ref.watch(currentProjectProvider); return Drawer( - child: NeumorphicBackground( - child: Column( - children: [ - DrawerHeader( - decoration: BoxDecoration( - color: theme.colorScheme.primary, + child: Column( + children: [ + DrawerHeader( + // decoration: BoxDecoration( + // color: theme.colorScheme.primary, + // ), + child: Center( + child: projects.when( + data: (data) { + return currentProject.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const NeumorphicProgressIndeterminate(), + data: (current) { + return DropdownButtonFormField( + isExpanded: true, + value: current, + icon: Icon(Icons.expand_more, + color: theme.colorScheme.onPrimary), + onChanged: (Project? value) { + ref + .read(currentProjectProvider.notifier) + .setCurrent(value); + }, + items: data + .map>((Project value) { + return DropdownMenuItem( + value: value, + child: Text( + value.name ?? '--', + ), + ); + }).toList(), + ); + }); + }, + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), ), - child: Center( - child: projects.when( - data: (data) { - return currentProject.when( - error: (error, stackTrace) => SizedBox( - child: Text(error.toString()), - ), - loading: () => const NeumorphicProgressIndeterminate(), - data: (current) { - return Neumorphic( - style: neumorphicTheme - .getNeumorphicStyle() - .copyWith(color: theme.colorScheme.primary), - padding: const EdgeInsets.symmetric(horizontal: 24), - child: DropdownButtonFormField( - isExpanded: true, - dropdownColor: theme.colorScheme.primary, - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.onPrimary), - value: current, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onPrimary), - onChanged: (Project? value) { - ref - .read(currentProjectProvider.notifier) - .setCurrent(value); - }, - items: data.map>( - (Project value) { - return DropdownMenuItem( - value: value, - child: Text(value.name ?? '--', - style: theme.textTheme.titleMedium! - .copyWith( - color: - theme.colorScheme.onPrimary)), - ); - }).toList(), - ), - ); - }); - }, - error: (error, stackTrace) => SizedBox( - child: Text(error.toString()), + loading: () => const NeumorphicProgressIndeterminate(), + )), + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + leading: const Icon(Icons.dashboard), + title: const Text('Dashboard'), + selected: false, + onTap: () { + router.goNamed(HistoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, ), - loading: () => const NeumorphicProgressIndeterminate(), - )), - ), - Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: [ - ListTile( - leading: const Icon(Icons.dashboard), - title: const Text('Dashboard'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(HistoryScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.done_all), - title: const Text('Task Templates'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(TemplateScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.inventory), - title: const Text('Inventory'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(InventoryScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.monitor), - title: const Text('Environment'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(EnvironmentScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.key), - title: const Text('Key Store'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(KeyStoreScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.gite), - title: const Text('Repositories'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(RepositoryScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ListTile( - leading: const Icon(Icons.group_work), - title: const Text('Team'), - selected: false, - onTap: () { - Navigator.pop(context); - router.goNamed(TeamScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' - }); - }, - ), - ], - ), - ), - SafeArea( - child: Container( - margin: const EdgeInsets.all(24), - child: NeumorphicToggle( - height: 48, - selectedIndex: themeMode.index, - displayForegroundOnlyIfSelected: true, - thumb: Neumorphic( - style: NeumorphicStyle( - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12))), - )), - children: ThemeMode.values - .map((t) => ToggleElement( - background: Center(child: t.icon), - foreground: Center(child: t.icon), - )) - .toList(), - onChanged: (i) { - ref - .read(themeModeProvider.notifier) - .changeThemeMode(ThemeMode.values[i]); - }, - )), + ListTile( + leading: const Icon(Icons.done_all), + title: const Text('Task Templates'), + selected: false, + onTap: () { + router.goNamed(TemplateScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ListTile( + leading: const Icon(Icons.inventory), + title: const Text('Inventory'), + selected: false, + onTap: () { + router.goNamed(InventoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ListTile( + leading: const Icon(Icons.monitor), + title: const Text('Environment'), + selected: false, + onTap: () { + router.goNamed(EnvironmentScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ListTile( + leading: const Icon(Icons.key), + title: const Text('Key Store'), + selected: false, + onTap: () { + router.goNamed(KeyStoreScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ListTile( + leading: const Icon(Icons.gite), + title: const Text('Repositories'), + selected: false, + onTap: () { + router.goNamed(RepositoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ListTile( + leading: const Icon(Icons.group_work), + title: const Text('Team'), + selected: false, + onTap: () { + router.goNamed(TeamScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/components/environment/form.dart b/lib/components/environment/form.dart index 60d480b..148a154 100644 --- a/lib/components/environment/form.dart +++ b/lib/components/environment/form.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/environment.dart'; class EnvironmentForm extends ConsumerWidget { @@ -10,127 +14,133 @@ class EnvironmentForm extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); final environment = ref.watch(environmentFamily(environmentId)); final formData = ref.watch(environmentFormRequestProvider(environment.value)); + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + return environment.when( data: (environment) { return SingleChildScrollView( - child: Form( - child: Column(children: [ - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(24), - child: Text( - environment.id == null - ? 'New Environment' - : 'Edit Environment', - style: theme.textTheme.titleLarge), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.name, - onChanged: (value) { - ref - .read( - environmentFormRequestProvider(environment).notifier) - .updateWith(name: value); - }, - decoration: const InputDecoration( - labelText: 'Name', contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.json, - onChanged: (value) { - ref - .read( - environmentFormRequestProvider(environment).notifier) - .updateWith(json: value); - }, - minLines: 5, - maxLines: null, - decoration: const InputDecoration( - labelText: 'Extra variables', - alignLabelWithHint: true, - hintText: 'Enter extra variables JSON...', - contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.env, - onChanged: (value) { - ref - .read( - environmentFormRequestProvider(environment).notifier) - .updateWith(env: value); - }, - minLines: 5, - maxLines: null, - decoration: const InputDecoration( - labelText: 'Environment variables', - alignLabelWithHint: true, - hintText: 'Enter env JSON...', - contentPadding: EdgeInsets.all(8)), - ), - Neumorphic( - style: neumorphicTheme.styleWith(depth: -4), - margin: const EdgeInsets.all(24), - padding: const EdgeInsets.all(24), - child: SelectableText.rich(TextSpan(children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Icon(Icons.info_rounded, - color: theme.colorScheme.primary), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(24), + child: Text( + environment.id == null + ? 'New Environment' + : 'Edit Environment', + style: textTitleStyle), ), - ), - const TextSpan( - text: - 'Environment and extra variables must be valid JSON. Example:\n'), - TextSpan(text: '''{ - "ANSIBLE_HOST_KEY_CHECKING": "false", - "ANSIBLE_GATHER_SUBSET": "!all" -}''', style: theme.textTheme.bodyMedium) - ])), - ), - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - alignment: Alignment.bottomRight, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { + ref + .read(environmentFormRequestProvider(environment) + .notifier) + .updateWith(name: value); }, - child: const Text('Cancel')), - const SizedBox( - width: 24, - ), - NeumorphicButton( - onPressed: () async { - await ref + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.json, + onChanged: (value) { + ref .read(environmentFormRequestProvider(environment) .notifier) - .postEnvironment(); - if (context.mounted) Navigator.of(context).pop(); - ref.read(environmentListProvider.notifier).loadRows(); + .updateWith(json: value); }, - child: const Text('Create')), - ]), - ), - ]), + minLines: 5, + maxLines: null, + decoration: const InputDecoration( + labelText: 'Extra variables', + alignLabelWithHint: true, + hintText: 'Enter extra variables JSON...', + contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.env, + onChanged: (value) { + ref + .read(environmentFormRequestProvider(environment) + .notifier) + .updateWith(env: value); + }, + minLines: 5, + maxLines: null, + decoration: const InputDecoration( + labelText: 'Environment variables', + alignLabelWithHint: true, + hintText: 'Enter env JSON...', + contentPadding: EdgeInsets.all(8)), + ), + SelectableText.rich(TextSpan(children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Icon(Icons.info_rounded, + color: theme.colorScheme.primary), + ), + ), + TextSpan( + text: + 'Environment and extra variables must be valid JSON. Example:\n', + style: textBodyStyle), + TextSpan(text: '''{ + "ANSIBLE_HOST_KEY_CHECKING": "false", + "ANSIBLE_GATHER_SUBSET": "!all" +}''', style: textBodyStyle) + ])), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.bottomRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveTextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + const SizedBox( + width: 24, + ), + AdaptiveButton( + onPressed: () async { + await ref + .read(environmentFormRequestProvider( + environment) + .notifier) + .postEnvironment(); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(environmentListProvider.notifier) + .loadRows(); + }, + child: const Text('Create')), + ]), + ), + ]), + ), ), ); }, diff --git a/lib/components/environment_name.dart b/lib/components/environment_name.dart index 039e6c4..fe77cb0 100644 --- a/lib/components/environment_name.dart +++ b/lib/components/environment_name.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:semaphore/state/projects/environment.dart'; class EnvironmentName extends ConsumerWidget { @@ -12,11 +15,16 @@ class EnvironmentName extends ConsumerWidget { final environment = ref.watch(environmentFamily(id)); return environment.when( - data: (environment) => Text( - environment.name ?? '--', - ), + data: (environment) => Platform.isMacOS + ? Text(environment.name ?? '--', + style: MacosTheme.of(context).typography.body) + : Text( + environment.name ?? '--', + ), loading: () => const LinearProgressIndicator(), - error: (error, stackTrace) => const Text('N/A'), + error: (error, stackTrace) => Platform.isMacOS + ? Text('N/A', style: MacosTheme.of(context).typography.body) + : const Text('N/A'), ); } } diff --git a/lib/components/inventory/form.dart b/lib/components/inventory/form.dart index 9606d47..3ce0ad9 100644 --- a/lib/components/inventory/form.dart +++ b/lib/components/inventory/form.dart @@ -1,7 +1,14 @@ +import 'dart:io'; + import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dropdown.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/access_key.dart'; import 'package:semaphore/state/projects/inventory.dart'; @@ -12,193 +19,171 @@ class InventoryForm extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); final inventory = ref.watch(inventoryFamily(inventoryId)); final formData = ref.watch(inventoryFormRequestProvider(inventory.value)); final accessKey = ref.watch(accessKeyProvider); + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + return inventory.when( data: (inventory) { - return Form( - child: Column(children: [ - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(24), - child: Text( - inventory.id == null ? 'New Inventory' : 'Edit Inventory', - style: theme.textTheme.titleLarge), - ), - Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData.name, - onChanged: (value) { - ref - .read(inventoryFormRequestProvider(inventory).notifier) - .updateWith(name: value); - }, - decoration: const InputDecoration( - labelText: 'Name', contentPadding: EdgeInsets.all(8)), - ), - ), - accessKey.when( - data: (data) { - return Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - // padding: const EdgeInsets.symmetric(horizontal: 24), - child: DropdownButtonFormField( - isExpanded: true, - dropdownColor: theme.colorScheme.secondaryContainer, - style: theme.textTheme.titleMedium!.copyWith( - color: theme.colorScheme.onSecondaryContainer), + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(24), + child: Text( + inventory.id == null + ? 'New Inventory' + : 'Edit Inventory', + style: textTitleStyle), + ), + AdaptiveTextField( + initialValue: formData.name, + onChanged: (value) { + ref + .read(inventoryFormRequestProvider(inventory) + .notifier) + .updateWith(name: value); + }, + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + accessKey.when( + data: (data) { + return AdaptiveDropdownMenu( + decoration: const InputDecoration( + labelText: 'User Credentials', + contentPadding: EdgeInsets.all(8)), + value: formData.sshKeyId, + onChanged: (int? value) { + ref + .read(inventoryFormRequestProvider(inventory) + .notifier) + .updateWith(sshKeyId: value); + }, + items: data.map>( + (AccessKey value) { + return AdaptiveDropdownMenuItem( + value: value.id, + child: Text(value.name ?? '--', + style: textBodyStyle), + ); + }).toList(), + ); + }, + loading: () => + const Center(child: LinearProgressIndicator()), + error: (error, stack) => const Text('Error')), + accessKey.when( + data: (data) { + return AdaptiveDropdownMenu( + decoration: InputDecoration( + labelText: 'Sudo Credentials (Optional)', + contentPadding: const EdgeInsets.all(8), + suffixIcon: formData.becomeKeyId == null + ? null + : AdaptiveIconButton( + iconData: (Icons.clear), + onPressed: () { + ref + .read(inventoryFormRequestProvider( + inventory) + .notifier) + .unsetWith(becomeKeyId: true); + }), + ), + value: formData.becomeKeyId, + onChanged: (int? value) { + ref + .read(inventoryFormRequestProvider(inventory) + .notifier) + .updateWith(becomeKeyId: value); + }, + items: data + .where((element) => + element.type == + AccessKeyTypeEnum.loginPassword) + .map>( + (AccessKey value) { + return AdaptiveDropdownMenuItem( + value: value.id, + child: Text( + value.name ?? '--', + style: textBodyStyle, + )); + }).toList(), + ); + }, + loading: () => + const Center(child: LinearProgressIndicator()), + error: (error, stack) => const Text('Error')), + AdaptiveDropdownMenu( decoration: const InputDecoration( - labelText: 'User Credentials', - contentPadding: EdgeInsets.all(8)), - value: formData.sshKeyId, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onSecondaryContainer), - onChanged: (int? value) { + labelText: 'Type', contentPadding: EdgeInsets.all(8)), + value: formData.type, + onChanged: (InventoryRequestTypeEnum? value) { ref .read(inventoryFormRequestProvider(inventory) .notifier) - .updateWith(sshKeyId: value); + .updateWith(type: value); }, - items: data.map>((AccessKey value) { - return DropdownMenuItem( - value: value.id, - child: Text(value.name ?? '--', - style: theme.textTheme.titleMedium!.copyWith( - color: - theme.colorScheme.onSecondaryContainer)), + items: InventoryRequestTypeEnum.values.map< + AdaptiveDropdownMenuItem< + InventoryRequestTypeEnum>>( + (InventoryRequestTypeEnum value) { + return AdaptiveDropdownMenuItem< + InventoryRequestTypeEnum>( + value: value, + child: Text(value.name, style: textBodyStyle), ); }).toList(), ), - ); - }, - loading: () => const Center(child: LinearProgressIndicator()), - error: (error, stack) => const Text('Error')), - accessKey.when( - data: (data) { - return Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - // padding: const EdgeInsets.symmetric(horizontal: 24), - child: DropdownButtonFormField( - isExpanded: true, - dropdownColor: theme.colorScheme.secondaryContainer, - style: theme.textTheme.titleMedium!.copyWith( - color: theme.colorScheme.onSecondaryContainer), - decoration: InputDecoration( - labelText: 'Sudo Credentials (Optional)', - contentPadding: const EdgeInsets.all(8), - suffixIcon: formData.becomeKeyId == null - ? null - : IconButton( - icon: const Icon(Icons.clear), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.bottomRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveTextButton( onPressed: () { - ref + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + const SizedBox( + width: 24, + ), + AdaptiveButton( + onPressed: () async { + await ref .read(inventoryFormRequestProvider( inventory) .notifier) - .unsetWith(becomeKeyId: true); - }), - ), - value: formData.becomeKeyId, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onSecondaryContainer), - onChanged: (int? value) { - ref - .read(inventoryFormRequestProvider(inventory) - .notifier) - .updateWith(becomeKeyId: value); - }, - items: data - .where((element) => - element.type == AccessKeyTypeEnum.loginPassword) - .map>((AccessKey value) { - return DropdownMenuItem( - value: value.id, - child: Text(value.name ?? '--', - style: theme.textTheme.titleMedium!.copyWith( - color: - theme.colorScheme.onSecondaryContainer)), - ); - }).toList(), + .postInventory(); + if (context.mounted) + Navigator.of(context).pop(); + ref + .read(inventoryListProvider.notifier) + .loadRows(); + }, + child: const Text('Create')), + ]), ), - ); - }, - loading: () => const Center(child: LinearProgressIndicator()), - error: (error, stack) => const Text('Error')), - Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: DropdownButtonFormField( - isExpanded: true, - dropdownColor: theme.colorScheme.secondaryContainer, - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.onSecondaryContainer), - decoration: const InputDecoration( - labelText: 'Type', contentPadding: EdgeInsets.all(8)), - value: formData.type, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onSecondaryContainer), - onChanged: (InventoryRequestTypeEnum? value) { - ref - .read(inventoryFormRequestProvider(inventory).notifier) - .updateWith(type: value); - }, - items: InventoryRequestTypeEnum.values - .map>( - (InventoryRequestTypeEnum value) { - return DropdownMenuItem( - value: value, - child: Text(value.name, - style: theme.textTheme.titleMedium!.copyWith( - color: theme.colorScheme.onSecondaryContainer)), - ); - }).toList(), - ), - ), - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - alignment: Alignment.bottomRight, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - const SizedBox( - width: 24, - ), - NeumorphicButton( - onPressed: () async { - await ref - .read( - inventoryFormRequestProvider(inventory).notifier) - .postInventory(); - if (context.mounted) Navigator.of(context).pop(); - ref.read(inventoryListProvider.notifier).loadRows(); - }, - child: const Text('Create')), - ]), + ]), ), - ]), + ), ); }, loading: () => const Center(child: CircularProgressIndicator()), diff --git a/lib/components/inventory_name.dart b/lib/components/inventory_name.dart index 895b2d7..8ea1fcf 100644 --- a/lib/components/inventory_name.dart +++ b/lib/components/inventory_name.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:semaphore/state/projects/inventory.dart'; class InventoryName extends ConsumerWidget { @@ -12,11 +15,16 @@ class InventoryName extends ConsumerWidget { final inventory = ref.watch(inventoryFamily(id)); return inventory.when( - data: (inventory) => Text( - inventory.name ?? '--', - ), + data: (inventory) => Platform.isMacOS + ? Text(inventory.name ?? '--', + style: MacosTheme.of(context).typography.body) + : Text( + inventory.name ?? '--', + ), loading: () => const LinearProgressIndicator(), - error: (error, stackTrace) => const Text('N/A'), + error: (error, stackTrace) => Platform.isMacOS + ? Text('N/A', style: MacosTheme.of(context).typography.body) + : const Text('N/A'), ); } } diff --git a/lib/components/macos/sidebar.dart b/lib/components/macos/sidebar.dart new file mode 100644 index 0000000..3f2ebe8 --- /dev/null +++ b/lib/components/macos/sidebar.dart @@ -0,0 +1,167 @@ +import 'package:ansible_semaphore/ansible_semaphore.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/router/router.dart'; +import 'package:semaphore/screens/project/environment_screen.dart'; +import 'package:semaphore/screens/project/history_screen.dart'; +import 'package:semaphore/screens/project/inventory_screen.dart'; +import 'package:semaphore/screens/project/keystore_screen.dart'; +import 'package:semaphore/screens/project/repository_screen.dart'; +import 'package:semaphore/screens/project/team_screen.dart'; +import 'package:semaphore/screens/project/template_screen.dart'; +import 'package:semaphore/screens/project/template_task_screen.dart'; +import 'package:semaphore/state/projects.dart'; + +Sidebar buildSidebar(BuildContext context) { + return Sidebar( + minWidth: 220, + top: MacosListTile( + leading: const Text('Project'), + title: Center(child: Consumer(builder: (context, ref, _) { + final projects = ref.watch(projectsProvider); + final currentProject = ref.watch(currentProjectProvider); + + return projects.when( + data: (data) { + return currentProject.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const ProgressCircle(), + data: (current) { + return MacosPopupButton( + value: current, + onChanged: (Project? value) { + ref + .read(currentProjectProvider.notifier) + .setCurrent(value); + }, + items: + data.map>((Project value) { + return MacosPopupMenuItem( + value: value, + child: Text( + value.name ?? '--', + ), + ); + }).toList(), + ); + }); + }, + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const ProgressCircle(), + ); + })), + ), + builder: (context, scrollController) { + return Consumer(builder: (context, ref, _) { + final router = ref.read(routerProvider); + final currentProject = ref.watch(currentProjectProvider); + final currnetName = GoRouterState.of(context).name; + + int idx = 0; + switch (currnetName) { + case 'templates': + case 'template_tasks': + idx = 1; + break; + case 'projectInventory': + idx = 2; + break; + case 'projectEnvironment': + idx = 3; + break; + case 'projectKeyStore': + idx = 4; + break; + case 'projectRepository': + idx = 5; + break; + case 'projectTeam': + idx = 6; + break; + default: + idx = 0; + } + return SidebarItems( + currentIndex: idx, + onChanged: (i) { + switch (i) { + case 0: + router.goNamed(HistoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 1: + router.goNamed(TemplateScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 2: + router.goNamed(InventoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 3: + router.goNamed(EnvironmentScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 4: + router.goNamed(KeyStoreScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 5: + router.goNamed(RepositoryScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + case 6: + router.goNamed(TeamScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + break; + } + }, + scrollController: scrollController, + itemSize: SidebarItemSize.large, + items: const [ + SidebarItem( + leading: MacosIcon(Icons.dashboard), + label: Text('Dashboard'), + ), + SidebarItem( + leading: MacosIcon(Icons.done_all), + label: Text('Task Templates'), + ), + SidebarItem( + leading: MacosIcon(Icons.inventory), + label: Text('Inventory'), + ), + SidebarItem( + leading: MacosIcon(Icons.monitor), + label: Text('Environment'), + ), + SidebarItem( + leading: MacosIcon(Icons.key), + label: Text('Key Store'), + ), + SidebarItem( + leading: MacosIcon(Icons.gite), + label: Text('Repositories'), + ), + SidebarItem( + leading: MacosIcon(Icons.group_work), + label: Text('Team'), + ), + ], + ); + }); + }, + ); +} diff --git a/lib/components/macos/text_field.dart b/lib/components/macos/text_field.dart new file mode 100644 index 0000000..7ba9d65 --- /dev/null +++ b/lib/components/macos/text_field.dart @@ -0,0 +1,385 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart' + show + InputDecoration, + InputCounterWidgetBuilder, + AdaptiveTextSelectionToolbar; +import 'package:macos_ui/macos_ui.dart' show MacosTextField; + +export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; + +/// A [FormField] that contains a [MacosTextField]. +/// +/// This is a convenience widget that wraps a [MacosTextField] widget in a +/// [FormField]. +/// +/// A [Form] ancestor is not required. The [Form] allows one to +/// save, reset, or validate multiple fields at once. To use without a [Form], +/// pass a `GlobalKey` (see [GlobalKey]) to the constructor and use +/// [GlobalKey.currentState] to save or reset the form field. +/// +/// When a [controller] is specified, its [TextEditingController.text] +/// defines the [initialValue]. If this [FormField] is part of a scrolling +/// container that lazily constructs its children, like a [ListView] or a +/// [CustomScrollView], then a [controller] should be specified. +/// The controller's lifetime should be managed by a stateful widget ancestor +/// of the scrolling container. +/// +/// If a [controller] is not specified, [initialValue] can be used to give +/// the automatically generated controller an initial value. +/// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// +/// Remember to call [TextEditingController.dispose] of the [TextEditingController] +/// when it is no longer needed. This will ensure any resources used by the object +/// are discarded. +/// +/// By default, `decoration` will apply the [ThemeData.inputDecorationTheme] for +/// the current context to the [InputDecoration], see +/// [InputDecoration.applyDefaults]. +/// +/// For a documentation about the various parameters, see [MacosTextField]. +/// +/// {@tool snippet} +/// +/// Creates a [TextFormField] with an [InputDecoration] and validator function. +/// +/// ![If the user enters valid text, the MacosTextField appears normally without any warnings to the user](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field.png) +/// +/// ![If the user enters invalid text, the error message returned from the validator function is displayed in dark red underneath the input](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field_error.png) +/// +/// ```dart +/// TextFormField( +/// decoration: const InputDecoration( +/// icon: Icon(Icons.person), +/// hintText: 'What do people call you?', +/// labelText: 'Name *', +/// ), +/// onSaved: (String? value) { +/// // This optional block of code can be used to run +/// // code when the user saves the form. +/// }, +/// validator: (String? value) { +/// return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null; +/// }, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows how to move the focus to the next field when the user +/// presses the SPACE key. +/// +/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * +/// * [MacosTextField], which is the underlying text field without the [Form] +/// integration. +/// * [InputDecorator], which shows the labels and other visual elements that +/// surround the actual text editing widget. +/// * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller). +class MacosTextFormField extends FormField { + /// Creates a [FormField] that contains a [MacosTextField]. + /// + /// When a [controller] is specified, [initialValue] must be null (the + /// default). If [controller] is null, then a [TextEditingController] + /// will be constructed automatically and its `text` will be initialized + /// to [initialValue] or the empty string. + /// + /// For documentation about the various parameters, see the [MacosTextField] class + /// and [MacosTextField.new], the constructor. + MacosTextFormField({ + super.key, + this.controller, + String? initialValue, + FocusNode? focusNode, + InputDecoration? decoration = const InputDecoration(), + TextInputType? keyboardType, + TextCapitalization textCapitalization = TextCapitalization.none, + TextInputAction? textInputAction, + TextStyle? style, + StrutStyle? strutStyle, + TextDirection? textDirection, + TextAlign textAlign = TextAlign.start, + TextAlignVertical? textAlignVertical, + bool autofocus = false, + bool readOnly = false, + @Deprecated( + 'Use `contextMenuBuilder` instead. ' + 'This feature was deprecated after v3.3.0-0.5.pre.', + ) + ToolbarOptions? toolbarOptions, + bool? showCursor, + String obscuringCharacter = '•', + bool obscureText = false, + bool autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + bool enableSuggestions = true, + MaxLengthEnforcement? maxLengthEnforcement, + int? maxLines = 1, + int? minLines, + bool expands = false, + int? maxLength, + ValueChanged? onChanged, + GestureTapCallback? onTap, + TapRegionCallback? onTapOutside, + VoidCallback? onEditingComplete, + ValueChanged? onFieldSubmitted, + super.onSaved, + super.validator, + List? inputFormatters, + bool? enabled, + double cursorWidth = 2.0, + double? cursorHeight, + Radius? cursorRadius, + Color? cursorColor, + Brightness? keyboardAppearance, + EdgeInsets scrollPadding = const EdgeInsets.all(20.0), + bool? enableInteractiveSelection, + TextSelectionControls? selectionControls, + InputCounterWidgetBuilder? buildCounter, + ScrollPhysics? scrollPhysics, + Iterable? autofillHints, + AutovalidateMode? autovalidateMode, + ScrollController? scrollController, + super.restorationId, + bool enableIMEPersonalizedLearning = true, + MouseCursor? mouseCursor, + EditableTextContextMenuBuilder? contextMenuBuilder = + _defaultContextMenuBuilder, + SpellCheckConfiguration? spellCheckConfiguration, + TextMagnifierConfiguration? magnifierConfiguration, + UndoHistoryController? undoController, + AppPrivateCommandCallback? onAppPrivateCommand, + bool? cursorOpacityAnimates, + ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + ContentInsertionConfiguration? contentInsertionConfiguration, + Clip clipBehavior = Clip.hardEdge, + bool scribbleEnabled = true, + bool canRequestFocus = true, + }) : assert(initialValue == null || controller == null), + assert(obscuringCharacter.length == 1), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert(!obscureText || maxLines == 1, + 'Obscured fields cannot be multiline.'), + assert(maxLength == null || maxLength > 0), + super( + initialValue: + controller != null ? controller.text : (initialValue ?? ''), + enabled: enabled ?? decoration?.enabled ?? true, + autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, + builder: (FormFieldState field) { + final _TextFormFieldState state = field as _TextFormFieldState; + + void onChangedHandler(String value) { + field.didChange(value); + if (onChanged != null) { + onChanged(value); + } + } + + return UnmanagedRestorationScope( + bucket: field.bucket, + child: MacosTextField( + restorationId: restorationId, + controller: state._effectiveController, + focusNode: focusNode, + keyboardType: keyboardType, + textInputAction: textInputAction, + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textCapitalization: textCapitalization, + autofocus: autofocus, + readOnly: readOnly, + showCursor: showCursor, + obscuringCharacter: obscuringCharacter, + obscureText: obscureText, + autocorrect: autocorrect, + smartDashesType: smartDashesType ?? + (obscureText + ? SmartDashesType.disabled + : SmartDashesType.enabled), + smartQuotesType: smartQuotesType ?? + (obscureText + ? SmartQuotesType.disabled + : SmartQuotesType.enabled), + enableSuggestions: enableSuggestions, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + expands: expands, + maxLength: maxLength, + onChanged: onChangedHandler, + onTap: onTap, + onEditingComplete: onEditingComplete, + onSubmitted: onFieldSubmitted, + inputFormatters: inputFormatters, + enabled: enabled ?? decoration?.enabled ?? true, + cursorWidth: cursorWidth, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius ?? const Radius.circular(2.0), + cursorColor: cursorColor, + scrollPadding: scrollPadding, + scrollPhysics: scrollPhysics, + keyboardAppearance: keyboardAppearance, + enableInteractiveSelection: + enableInteractiveSelection ?? (!obscureText || !readOnly), + selectionControls: selectionControls, + autofillHints: autofillHints, + scrollController: scrollController, + contextMenuBuilder: contextMenuBuilder, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + dragStartBehavior: dragStartBehavior, + ), + ); + }, + ); + + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController] and + /// initialize its [TextEditingController.text] with [initialValue]. + final TextEditingController? controller; + + static Widget _defaultContextMenuBuilder( + BuildContext context, EditableTextState editableTextState) { + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); + } + + @override + FormFieldState createState() => _TextFormFieldState(); +} + +class _TextFormFieldState extends FormFieldState { + RestorableTextEditingController? _controller; + + TextEditingController get _effectiveController => + _textFormField.controller ?? _controller!.value; + + MacosTextFormField get _textFormField => super.widget as MacosTextFormField; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + super.restoreState(oldBucket, initialRestore); + if (_controller != null) { + _registerController(); + } + // Make sure to update the internal [FormFieldState] value to sync up with + // text editing controller value. + setValue(_effectiveController.text); + } + + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } + + void _createLocalController([TextEditingValue? value]) { + assert(_controller == null); + _controller = value == null + ? RestorableTextEditingController() + : RestorableTextEditingController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } + + @override + void initState() { + super.initState(); + if (_textFormField.controller == null) { + _createLocalController(widget.initialValue != null + ? TextEditingValue(text: widget.initialValue!) + : null); + } else { + _textFormField.controller!.addListener(_handleControllerChanged); + } + } + + @override + void didUpdateWidget(MacosTextFormField oldWidget) { + super.didUpdateWidget(oldWidget); + if (_textFormField.controller != oldWidget.controller) { + oldWidget.controller?.removeListener(_handleControllerChanged); + _textFormField.controller?.addListener(_handleControllerChanged); + + if (oldWidget.controller != null && _textFormField.controller == null) { + _createLocalController(oldWidget.controller!.value); + } + + if (_textFormField.controller != null) { + setValue(_textFormField.controller!.text); + if (oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + } + } + } + + @override + void dispose() { + _textFormField.controller?.removeListener(_handleControllerChanged); + _controller?.dispose(); + super.dispose(); + } + + @override + void didChange(String? value) { + super.didChange(value); + + if (_effectiveController.text != value) { + _effectiveController.text = value ?? ''; + } + } + + @override + void reset() { + // setState will be called in the superclass, so even though state is being + // manipulated, no setState call is needed here. + _effectiveController.text = widget.initialValue ?? ''; + super.reset(); + } + + void _handleControllerChanged() { + // Suppress changes that originated from within this class. + // + // In the case where a controller has been passed in to this widget, we + // register this change listener. In these cases, we'll also receive change + // notifications for changes originating from within this class -- for + // example, the reset() method. In such cases, the FormField value will + // already have been set. + if (_effectiveController.text != value) { + didChange(_effectiveController.text); + } + } +} diff --git a/lib/components/repository.dart b/lib/components/repository.dart index 7e1cad9..0bd9d6b 100644 --- a/lib/components/repository.dart +++ b/lib/components/repository.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:semaphore/state/projects/repository.dart'; class RepositoryName extends ConsumerWidget { @@ -12,12 +15,18 @@ class RepositoryName extends ConsumerWidget { final repository = ref.watch(repositoryFamily(id)); return repository.when( - data: (repository) => Text( - repository.name ?? '--', - overflow: TextOverflow.ellipsis, - ), + data: (repository) => Platform.isMacOS + ? Text(repository.name ?? '--', + overflow: TextOverflow.ellipsis, + style: MacosTheme.of(context).typography.body) + : Text( + repository.name ?? '--', + overflow: TextOverflow.ellipsis, + ), loading: () => const LinearProgressIndicator(), - error: (error, stackTrace) => const Text('N/A'), + error: (error, stackTrace) => Platform.isMacOS + ? Text('N/A', style: MacosTheme.of(context).typography.body) + : const Text('N/A'), ); } } diff --git a/lib/components/repository/form.dart b/lib/components/repository/form.dart index 7ad7726..fccd6dc 100644 --- a/lib/components/repository/form.dart +++ b/lib/components/repository/form.dart @@ -1,7 +1,14 @@ +import 'dart:io'; + import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dropdown.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/access_key.dart'; import 'package:semaphore/state/projects/repository.dart'; @@ -12,141 +19,145 @@ class RepositoryForm extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); final repository = ref.watch(repositoryFamily(repositoryId)); final formData = ref.watch(repositoryFormRequestProvider(repository.value)); final accessKey = ref.watch(accessKeyProvider); + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + return repository.when( data: (repository) { return SingleChildScrollView( - child: Form( - child: Column(children: [ - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(24), - child: Text( - repository.id == null - ? 'New Repository' - : 'Edit Repository', - style: theme.textTheme.titleLarge), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.name, - onChanged: (value) { - ref - .read(repositoryFormRequestProvider(repository).notifier) - .updateWith(name: value); - }, - decoration: const InputDecoration( - labelText: 'Name', contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.gitUrl, - onChanged: (value) { - ref - .read(repositoryFormRequestProvider(repository).notifier) - .updateWith(gitUrl: value); - }, - decoration: const InputDecoration( - labelText: 'URL or Path', - contentPadding: EdgeInsets.all(8)), - ), - NeumorphicTextFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - autocorrect: false, - initialValue: formData.gitBranch, - onChanged: (value) { - ref - .read(repositoryFormRequestProvider(repository).notifier) - .updateWith(gitBranch: value); - }, - decoration: const InputDecoration( - labelText: 'Branch', contentPadding: EdgeInsets.all(8)), - ), - accessKey.when( - data: (data) { - return NeumorphicDropdownButtonFormField( - margin: const EdgeInsets.all(24), - depth: -4, - borderRadius: const BorderRadius.all(Radius.circular(12)), - isExpanded: true, - dropdownColor: theme.colorScheme.secondaryContainer, - style: theme.textTheme.titleMedium!.copyWith( - color: theme.colorScheme.onSecondaryContainer), - decoration: InputDecoration( - labelText: 'Access Key', - contentPadding: const EdgeInsets.all(8), - suffixIcon: formData.sshKeyId == null - ? null - : IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - ref - .read(repositoryFormRequestProvider( - repository) - .notifier) - .unsetWith(sshKeyId: true); - }), - ), - value: formData.sshKeyId, - icon: Icon(Icons.expand_more, - color: theme.colorScheme.onSecondaryContainer), - onChanged: (int? value) { + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(24), + child: Text( + repository.id == null + ? 'New Repository' + : 'Edit Repository', + style: textTitleStyle), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { ref .read(repositoryFormRequestProvider(repository) .notifier) - .updateWith(sshKeyId: value); + .updateWith(name: value); }, - items: data.map>((AccessKey value) { - return DropdownMenuItem( - value: value.id, - child: Text(value.name ?? '--', - style: theme.textTheme.titleMedium!.copyWith( - color: - theme.colorScheme.onSecondaryContainer)), - ); - }).toList(), - ); - }, - loading: () => const Center(child: LinearProgressIndicator()), - error: (error, stack) => const Text('Error')), - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - alignment: Alignment.bottomRight, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.gitUrl, + onChanged: (value) { + ref + .read(repositoryFormRequestProvider(repository) + .notifier) + .updateWith(gitUrl: value); }, - child: const Text('Cancel')), - const SizedBox( - width: 24, - ), - NeumorphicButton( - onPressed: () async { - await ref + decoration: const InputDecoration( + labelText: 'URL or Path', + contentPadding: EdgeInsets.all(8)), + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.gitBranch, + onChanged: (value) { + ref .read(repositoryFormRequestProvider(repository) .notifier) - .postRepository(); - if (context.mounted) Navigator.of(context).pop(); - ref.read(repositoryListProvider.notifier).loadRows(); + .updateWith(gitBranch: value); }, - child: const Text('Create')), - ]), - ), - ]), + decoration: const InputDecoration( + labelText: 'Branch', + contentPadding: EdgeInsets.all(8)), + ), + accessKey.when( + data: (data) { + return AdaptiveDropdownMenu( + decoration: InputDecoration( + labelText: 'Access Key', + contentPadding: const EdgeInsets.all(8), + suffixIcon: formData.sshKeyId == null + ? null + : AdaptiveIconButton( + iconData: (Icons.clear), + onPressed: () { + ref + .read(repositoryFormRequestProvider( + repository) + .notifier) + .unsetWith(sshKeyId: true); + }), + ), + value: formData.sshKeyId, + onChanged: (int? value) { + ref + .read( + repositoryFormRequestProvider(repository) + .notifier) + .updateWith(sshKeyId: value); + }, + items: data.map>( + (AccessKey value) { + return AdaptiveDropdownMenuItem( + value: value.id, + child: Text(value.name ?? '--'), + ); + }).toList(), + ); + }, + loading: () => + const Center(child: LinearProgressIndicator()), + error: (error, stack) => const Text('Error')), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.bottomRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveTextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + const SizedBox( + width: 24, + ), + AdaptiveButton( + onPressed: () async { + await ref + .read(repositoryFormRequestProvider( + repository) + .notifier) + .postRepository(); + if (context.mounted) + Navigator.of(context).pop(); + ref + .read(repositoryListProvider.notifier) + .loadRows(); + }, + child: const Text('Create')), + ]), + ), + ]), + ), ), ); }, diff --git a/lib/components/run_task_form.dart b/lib/components/run_task_form.dart index f3b493c..e45a6a3 100644 --- a/lib/components/run_task_form.dart +++ b/lib/components/run_task_form.dart @@ -1,8 +1,12 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/checkbox.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/template.dart'; import 'package:semaphore/state/projects/template_task.dart'; @@ -13,160 +17,168 @@ class RunTaskFrom extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); + final template = ref.watch(templateFamilyProvider(templateId)); final formData = ref.watch(runTaskFormDataProvider); + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + return template.when( data: (template) { - return Form( - child: Column(children: [ - Container( - padding: const EdgeInsets.all(24), - child: Text(template.name!, style: theme.textTheme.titleLarge), - ), - Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData.message, - onChanged: (value) => ref - .read(runTaskFormDataProvider.notifier) - .updateWith(message: value), - decoration: const InputDecoration( - labelText: 'Message(Optional)', - contentPadding: EdgeInsets.all(8)), - ), - ), - ...(template.surveyVars?.map((surveyVar) { - return Neumorphic( - margin: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData.environment[surveyVar.name!], - onChanged: (value) { - ref - .read(runTaskFormDataProvider.notifier) - .updateSurveyVal(surveyVar.name!, value); - }, - decoration: InputDecoration( - labelText: surveyVar.title, - contentPadding: const EdgeInsets.all(8)), + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(24), + child: Text(template.name!, style: textTitleStyle), ), - ); - }).toList() ?? - []), - Container( - padding: const EdgeInsets.all(24), - child: Row( - children: [ - NeumorphicCheckbox( - value: formData.debug, - onChanged: (value) { - ref - .read(runTaskFormDataProvider.notifier) - .updateWith(debug: value); - }), - const SizedBox( - width: 24, - ), - Text('Debug', style: theme.textTheme.bodyMedium), - ], - ), - ), - Container( - padding: const EdgeInsets.all(24), - child: Row( - children: [ - NeumorphicCheckbox( - value: formData.dryRun, - onChanged: (value) { - ref - .read(runTaskFormDataProvider.notifier) - .updateWith(dryRun: value); - }), - const SizedBox( - width: 24, - ), - Text('Dry Run', style: theme.textTheme.bodyMedium), - ], - ), - ), - Container( - padding: const EdgeInsets.all(24), - child: Row( - children: [ - NeumorphicCheckbox( - value: formData.diff, - onChanged: (value) { - ref - .read(runTaskFormDataProvider.notifier) - .updateWith(diff: value); - }), - const SizedBox( - width: 24, - ), - Text('Diff', style: theme.textTheme.bodyMedium), - ], - ), - ), - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - alignment: Alignment.bottomRight, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - const SizedBox( - width: 24, - ), - NeumorphicButton( - onPressed: formData.isSubmitting - ? null - : () async { - try { - ref - .read(runTaskFormDataProvider.notifier) - .updateWith(isSubmitting: true); - await ref - .read(templateTaskListProvider( - templateId: templateId) - .notifier) - .runTask( - templateId: templateId, - debug: formData.debug, - dryRun: formData.dryRun, - diff: formData.diff, - playbook: formData.playbook, - environment: json.encode( - formData.environment, - ), - limit: formData.limit, - ); - if (context.mounted) Navigator.of(context).pop(); - } catch (e) { - ref - .read(runTaskFormDataProvider.notifier) - .updateWith(errorString: e.toString()); - } finally { + AdaptiveTextField( + initialValue: formData.message, + onChanged: (value) => ref + .read(runTaskFormDataProvider.notifier) + .updateWith(message: value), + decoration: const InputDecoration( + labelText: 'Message(Optional)', + contentPadding: EdgeInsets.all(8)), + ), + ...(template.surveyVars?.map((surveyVar) { + return AdaptiveTextField( + initialValue: formData.environment[surveyVar.name!], + onChanged: (value) { ref .read(runTaskFormDataProvider.notifier) - .updateWith(isSubmitting: false); - } - }, - child: const Text('Run')), - ]), + .updateSurveyVal(surveyVar.name!, value); + }, + decoration: InputDecoration( + labelText: surveyVar.title, + contentPadding: const EdgeInsets.all(8)), + ); + }).toList() ?? + []), + Container( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + AdaptiveCheckbox( + value: formData.debug, + onChanged: (value) { + ref + .read(runTaskFormDataProvider.notifier) + .updateWith(debug: value); + }), + const SizedBox( + width: 24, + ), + Text('Debug', style: textBodyStyle), + ], + ), + ), + Container( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + AdaptiveCheckbox( + value: formData.dryRun, + onChanged: (value) { + ref + .read(runTaskFormDataProvider.notifier) + .updateWith(dryRun: value); + }), + const SizedBox( + width: 24, + ), + Text('Dry Run', style: textBodyStyle), + ], + ), + ), + Container( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + AdaptiveCheckbox( + value: formData.diff, + onChanged: (value) { + ref + .read(runTaskFormDataProvider.notifier) + .updateWith(diff: value); + }), + const SizedBox( + width: 24, + ), + Text('Diff', style: textBodyStyle), + ], + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + alignment: Alignment.bottomRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveTextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + const SizedBox( + width: 24, + ), + AdaptiveButton( + onPressed: formData.isSubmitting + ? null + : () async { + try { + ref + .read(runTaskFormDataProvider + .notifier) + .updateWith(isSubmitting: true); + await ref + .read(templateTaskListProvider( + templateId: templateId) + .notifier) + .runTask( + templateId: templateId, + debug: formData.debug, + dryRun: formData.dryRun, + diff: formData.diff, + playbook: formData.playbook, + environment: json.encode( + formData.environment, + ), + limit: formData.limit, + ); + if (context.mounted) + Navigator.of(context).pop(); + } catch (e) { + ref + .read(runTaskFormDataProvider + .notifier) + .updateWith( + errorString: e.toString()); + } finally { + ref + .read(runTaskFormDataProvider + .notifier) + .updateWith(isSubmitting: false); + } + }, + child: const Text('Run')), + ]), + ), + ]), ), - ]), + ), ); }, loading: () => const Center(child: CircularProgressIndicator()), diff --git a/lib/components/task_output_view.dart b/lib/components/task_output_view.dart index b25f23d..51767ac 100644 --- a/lib/components/task_output_view.dart +++ b/lib/components/task_output_view.dart @@ -1,7 +1,11 @@ +import 'dart:io' show Platform; import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/status_chip.dart'; import 'package:semaphore/state/projects/task.dart'; @@ -13,13 +17,26 @@ class TaskOutputView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); final task = ref.watch(taskFamilyProvider(id)); final taskOutput = ref.watch(taskOutputStreamProvider(id)); + Color bgColor = theme.colorScheme.surface; + Color textColor = theme.colorScheme.onSurface; + + bool isDark = theme.brightness.isDark; + + if (Platform.isMacOS) { + bgColor = macosTheme.canvasColor; + textColor = macosTheme.typography.body.color ?? macosTheme.primaryColor; + + isDark = macosTheme.brightness.isDark; + } + return task.when( data: (task) { return Container( - color: theme.colorScheme.surface, + color: bgColor, padding: const EdgeInsets.all(16), child: Column( children: [ @@ -27,61 +44,72 @@ class TaskOutputView extends ConsumerWidget { children: [ Text( 'Task #${task.id}', - style: Theme.of(context).textTheme.titleLarge, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: textColor), ), const SizedBox(width: 16), - StatusChip(status: task.status), + Material( + color: bgColor, + child: StatusChip(status: task.status), + ), const Spacer(), - IconButton( + AdaptiveIconButton( onPressed: () { Navigator.of(context).pop(); }, - icon: const Icon(Icons.close)) + iconData: (Icons.close)) ], ), const SizedBox(height: 12), Expanded( child: taskOutput.when( - data: (List taskOutput) => ListView( - children: taskOutput - .map((line) => Row( - children: [ - Text(line.time?.toLocal().toString() ?? '', - style: theme.textTheme.bodyMedium! - .copyWith( - color: theme.colorScheme - .onSurfaceVariant - .withOpacity(0.7))), - const SizedBox(width: 16), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - line.output?.replaceAll( + data: (List taskOutput) => + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: taskOutput + .map((line) => SelectableText.rich(TextSpan( + text: line.time?.toLocal().toString() ?? + '', + style: GoogleFonts.robotoMono( + textStyle: TextStyle( + color: isDark + ? Colors.lightGreenAccent + : Colors.lightGreen), + ), + children: [ + const TextSpan(text: '\t'), + TextSpan( + text: line.output?.replaceAll( RegExp( r'\u001b\[([0-9;]+)m'), '') ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: theme.colorScheme - .onSurface)), - ), - ), - ], - )) - .toList()), - loading: () => - const Center(child: CircularProgressIndicator()), + style: GoogleFonts.robotoMono( + textStyle: + TextStyle(color: textColor), + ), + ), + ]))) + .toList(), + ), + ), + loading: () => Center( + child: Platform.isMacOS + ? const ProgressCircle() + : const CircularProgressIndicator()), error: (error, stackTrace) => const Text('N/A')), ), ], ), ); }, - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => Center( + child: Platform.isMacOS + ? const ProgressCircle() + : const CircularProgressIndicator()), error: (error, stackTrace) => const Text('N/A'), ); } diff --git a/lib/main.dart b/lib/main.dart index e35d2d7..de4e7cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,24 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart' show MacosWindowUtilsConfig; import 'package:semaphore/utils/state_logger.dart'; import 'package:semaphore/app.dart'; +import 'package:system_theme/system_theme.dart'; // import 'package:semaphore/database/database.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await SystemTheme.accentColor.load(); + + if (!kIsWeb) { + if (Platform.isMacOS) { + const config = MacosWindowUtilsConfig(); + await config.apply(); + } + } // await Database.initialize(); // await Database.performMigrationIfNeeded(); diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index f13b4e5..9e04dc8 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/router/router.dart'; @@ -16,54 +16,44 @@ class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; - final size = MediaQuery.of(context).size; final width = size.width > size.height ? size.height : size.width; final textSize = width * 0.618 * 0.1; + ref.read(projectsProvider.notifier).getProjects(); final currentProject = ref.watch(currentProjectProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(), - body: NeumorphicBackground( - child: SafeArea( - child: Center( - child: Neumorphic( - style: neumorphicTheme - .getNeumorphicStyle() - .copyWith(boxShape: const NeumorphicBoxShape.circle()), - child: SizedBox( - width: width * 0.618, - height: width * 0.618, - child: Center( - child: currentProject.when( - data: (current) { - Timer(const Duration(milliseconds: 618), () { - ref.read(routerProvider).goNamed(HistoryScreen.name, - pathParameters: { - 'pid': '${current?.id ?? 1}', - }); + body: SafeArea( + child: Center( + child: SizedBox( + width: width * 0.618, + height: width * 0.618, + child: Center( + child: currentProject.when( + data: (current) { + Timer(const Duration(milliseconds: 618), () { + ref.read(routerProvider).goNamed(HistoryScreen.name, + pathParameters: { + 'pid': '${current?.id ?? 1}', }); - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - NeumorphicText('Ansible', - textStyle: TextStyle(fontSize: textSize)), - SizedBox(height: textSize), - NeumorphicText('Semaphore', - textStyle: TextStyle(fontSize: textSize)), - ], - )); - }, - error: (error, stackTrace) => Text(error.toString()), - loading: () => const CircularProgressIndicator(), - ), - ))), - ), + }); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Ansible', style: TextStyle(fontSize: textSize)), + SizedBox(height: textSize), + Text('Semaphore', style: TextStyle(fontSize: textSize)), + ], + )); + }, + error: (error, stackTrace) => Text(error.toString()), + loading: () => const CircularProgressIndicator(), + ), + )), ), ), ); diff --git a/lib/screens/project/environment_screen.dart b/lib/screens/project/environment_screen.dart index c6f3c70..a986155 100644 --- a/lib/screens/project/environment_screen.dart +++ b/lib/screens/project/environment_screen.dart @@ -1,7 +1,13 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/components/environment/form.dart'; @@ -15,43 +21,36 @@ class EnvironmentScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; // final currentProject = ref.watch(currentProjectProvider); final environmentList = ref.watch(environmentListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Environment'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.add), + label: 'Add', onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - child: const EnvironmentForm(), - ); - }); + adaptiveDialog( + context: context, + child: const EnvironmentForm(), + ); }, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: environmentList.columns, - rows: environmentList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(environmentListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: environmentList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: environmentList.columns, + rows: environmentList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(environmentListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: environmentList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/history_screen.dart b/lib/screens/project/history_screen.dart index b3b3ad9..48137c8 100644 --- a/lib/screens/project/history_screen.dart +++ b/lib/screens/project/history_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/state/projects/task.dart'; @@ -14,28 +15,25 @@ class HistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; final taskList = ref.watch(taskListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Dashboard'), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: taskList.columns, - rows: taskList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(taskListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: taskList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: taskList.columns, + rows: taskList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(taskListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: taskList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/inventory_screen.dart b/lib/screens/project/inventory_screen.dart index 0a29c2c..dca6812 100644 --- a/lib/screens/project/inventory_screen.dart +++ b/lib/screens/project/inventory_screen.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; @@ -15,44 +19,33 @@ class InventoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - // final neumorphicTheme = theme.extension()!; - - // final currentProject = ref.watch(currentProjectProvider); final inventoryList = ref.watch(inventoryListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Inventory'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.add), onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - child: const InventoryForm(), - ); - }); + adaptiveDialog( + context: context, + child: const InventoryForm(), + ); }, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: inventoryList.columns, - rows: inventoryList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(inventoryListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: inventoryList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: inventoryList.columns, + rows: inventoryList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(inventoryListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: inventoryList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/keystore_screen.dart b/lib/screens/project/keystore_screen.dart index 90d790d..e1744b8 100644 --- a/lib/screens/project/keystore_screen.dart +++ b/lib/screens/project/keystore_screen.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/access_key/form.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; @@ -14,44 +18,33 @@ class KeyStoreScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; - - // final currentProject = ref.watch(currentProjectProvider); final accessKeyList = ref.watch(accessKeyListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Key Store'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.add), onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - child: const AccessKeyForm(), - ); - }); + adaptiveDialog( + context: context, + child: const AccessKeyForm(), + ); }, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: accessKeyList.columns, - rows: accessKeyList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(accessKeyListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: accessKeyList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: accessKeyList.columns, + rows: accessKeyList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(accessKeyListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: accessKeyList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/repository_screen.dart b/lib/screens/project/repository_screen.dart index ea3f558..5c55042 100644 --- a/lib/screens/project/repository_screen.dart +++ b/lib/screens/project/repository_screen.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/components/repository/form.dart'; @@ -14,44 +18,33 @@ class RepositoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; - - // final currentProject = ref.watch(currentProjectProvider); final repositoryList = ref.watch(repositoryListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Repository'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.add), onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - child: const RepositoryForm(), - ); - }); + adaptiveDialog( + context: context, + child: const RepositoryForm(), + ); }, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: repositoryList.columns, - rows: repositoryList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(repositoryListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: repositoryList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: repositoryList.columns, + rows: repositoryList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(repositoryListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: repositoryList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/team_screen.dart b/lib/screens/project/team_screen.dart index 950fa3d..1519fa7 100644 --- a/lib/screens/project/team_screen.dart +++ b/lib/screens/project/team_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/state/projects/user.dart'; @@ -14,33 +16,30 @@ class TeamScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; // final currentProject = ref.watch(currentProjectProvider); final userList = ref.watch(userListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Team'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const Icon(Icons.add), onPressed: () {}, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: userList.columns, - rows: userList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(userListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: userList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: userList.columns, + rows: userList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(userListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: userList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/template_screen.dart b/lib/screens/project/template_screen.dart index e084ba8..38d4a39 100644 --- a/lib/screens/project/template_screen.dart +++ b/lib/screens/project/template_screen.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/state/projects/template.dart'; @@ -14,33 +17,30 @@ class TemplateScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - // final neumorphicTheme = theme.extension()!; // final currentProject = ref.watch(currentProjectProvider); final templateList = ref.watch(templateListProvider); - return Scaffold( + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Task Template'), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.add), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.add), onPressed: () {}, ), - body: NeumorphicBackground( - child: SafeArea( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: templateList.columns, - rows: templateList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(templateListProvider.notifier) - .setStateManager(event.stateManager); - }, - onChanged: (PlutoGridOnChangedEvent event) {}, - configuration: templateList.configurationWithTheme(theme), - ), + body: SafeArea( + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: templateList.columns, + rows: templateList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(templateListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: templateList.configurationWithTheme(context), ), ), ); diff --git a/lib/screens/project/template_task_screen.dart b/lib/screens/project/template_task_screen.dart index bdf2146..0545e2f 100644 --- a/lib/screens/project/template_task_screen.dart +++ b/lib/screens/project/template_task_screen.dart @@ -1,7 +1,13 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/floatingAction.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/components/environment_name.dart'; @@ -20,14 +26,21 @@ class TemplateTaskScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; + final macosTheme = MacosTheme.of(context); // final currentProject = ref.watch(currentProjectProvider); final template = ref.watch(templateFamilyProvider(templateId)); final taskList = ref.watch(templateTaskListProvider(templateId: templateId)); - return Scaffold( + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.title2 + : theme.textTheme.titleMedium; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + + return AdaptiveScaffold( drawer: const LocalDrawer(), appBar: template.when( data: (template) => @@ -35,85 +48,86 @@ class TemplateTaskScreen extends ConsumerWidget { loading: () => const LocalAppBar(title: 'Task Template Loading'), error: (error, stack) => const LocalAppBar(title: 'Task Error'), ), - floatingActionButton: NeumorphicFloatingActionButton( - child: const Icon(Icons.play_arrow), + floatingAction: AdaptiveFloatingAction( + icon: const AdaptiveIcon(Icons.play_arrow), onPressed: () { ref .read(templateTaskListProvider(templateId: templateId).notifier) .prepareRunTask(context); }, ), - body: NeumorphicBackground( - child: SafeArea( - child: Column( - children: [ - const SizedBox(height: 12), - template.when( - data: (template) => Wrap( - alignment: WrapAlignment.start, - runAlignment: WrapAlignment.start, - spacing: 20, - runSpacing: 20, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Playbook', style: theme.textTheme.titleMedium), - Text(template.playbook ?? '--', - style: theme.textTheme.bodyMedium), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Type', style: theme.textTheme.titleMedium), - Text('Task', style: theme.textTheme.bodyMedium), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Inventory', style: theme.textTheme.titleMedium), - InventoryName(id: template.inventoryId), - ], + body: SafeArea( + child: Column( + children: [ + Expanded( + child: PlutoGrid( + createHeader: (stateManager) { + return template.when( + data: (template) => Padding( + padding: const EdgeInsets.all(12.0), + child: Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + spacing: 20, + runSpacing: 20, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Playbook', style: textTitleStyle), + Text(template.playbook ?? '--', + style: textBodyStyle), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Type', style: textTitleStyle), + Text('Task', style: textBodyStyle), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Inventory', style: textTitleStyle), + InventoryName(id: template.inventoryId), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Environment', style: textTitleStyle), + EnvironmentName(id: template.environmentId), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Repository', style: textTitleStyle), + RepositoryName(id: template.repositoryId), + ], + ), + ], + ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Environment', style: theme.textTheme.titleMedium), - EnvironmentName(id: template.environmentId), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Repository', style: theme.textTheme.titleMedium), - RepositoryName(id: template.repositoryId), - ], - ), - ], - ), - loading: () => const LinearProgressIndicator(), - error: (error, stack) => Text('Error: $error'), - ), - const SizedBox(height: 12), - Expanded( - child: PlutoGrid( - mode: PlutoGridMode.readOnly, - columns: taskList.columns, - rows: taskList.rows, - noRowsWidget: null, - onLoaded: (PlutoGridOnLoadedEvent event) { - ref - .read(templateTaskListProvider(templateId: templateId) - .notifier) - .setStateManager(event.stateManager); - }, - configuration: taskList.configurationWithTheme(theme), - ), + loading: () => const LinearProgressIndicator(), + error: (error, stack) => Text('Error: $error'), + ); + }, + mode: PlutoGridMode.readOnly, + columns: taskList.columns, + rows: taskList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(templateTaskListProvider(templateId: templateId) + .notifier) + .setStateManager(event.stateManager); + }, + configuration: taskList.configurationWithTheme(context), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/screens/sign_in/sign_in_screen.dart b/lib/screens/sign_in/sign_in_screen.dart index 399015d..c9030ef 100644 --- a/lib/screens/sign_in/sign_in_screen.dart +++ b/lib/screens/sign_in/sign_in_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/router/router.dart'; import 'package:semaphore/screens/splash/splash_screen.dart'; import 'package:semaphore/screens/url_config/url_config_screen.dart'; @@ -15,174 +18,127 @@ class SignInScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; final api = ref.watch(semaphoreApiProvider); final formData = ref.watch(signInFormProvider); final formError = ref.watch(signInFormErrorProvider); - return Scaffold( - body: NeumorphicBackground( - child: SafeArea( - child: Center( - child: SingleChildScrollView( - child: Neumorphic( - margin: const EdgeInsets.all(24), - padding: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(24)))), - child: AutofillGroup( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + return AdaptiveScaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + child: AutofillGroup( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 64, + child: Row( children: [ - SizedBox( - height: 64, - child: Row( - children: [ - Text('Sign In or', - style: theme.textTheme.titleLarge), - const SizedBox(width: 12), - NeumorphicButton( - style: neumorphicTheme - .getNeumorphicStyle() - .copyWith( - depth: -4, - boxShape: - NeumorphicBoxShape.roundRect( - const BorderRadius.all( - Radius.circular(24))), - color: theme.colorScheme.secondary), - onPressed: () { - ref - .read(routerProvider) - .go(UrlConfigScreen.path); - }, - child: Text( - 'Config API URL', - style: theme.textTheme.titleMedium! - .copyWith( - color: - theme.colorScheme.onSecondary), - )) - ], - ), - ), - Neumorphic( - margin: const EdgeInsets.symmetric(vertical: 12), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData.auth, - decoration: const InputDecoration( - labelText: 'Auth', - contentPadding: EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - ), - enableSuggestions: false, - autocorrect: false, - autofillHints: const [ - AutofillHints.username, - ], - onChanged: (value) { - ref - .read(signInFormProvider.notifier) - .updateWith(auth: value); - }, - ), - ), - Neumorphic( - margin: const EdgeInsets.symmetric(vertical: 12), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData.password, - decoration: const InputDecoration( - labelText: 'Password', - contentPadding: EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - ), - obscureText: true, - enableSuggestions: false, - autocorrect: false, - autofillHints: const [ - AutofillHints.password, - ], - onChanged: (value) { - ref - .read(signInFormProvider.notifier) - .updateWith(password: value); + Text('Sign In or', style: theme.textTheme.titleLarge), + const SizedBox(width: 12), + AdaptiveButton( + onPressed: () { + ref.read(routerProvider).go(UrlConfigScreen.path); }, - ), - ), - Center( - child: NeumorphicButton( - style: neumorphicTheme.styleWith( - color: theme.colorScheme.primary, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all( - Radius.circular(24))), - ), - onPressed: () async { - try { - ref - .read(signInFormErrorProvider.notifier) - .updateWith(null); + child: Text( + 'Config API URL', + style: theme.textTheme.titleMedium!.copyWith( + color: theme.colorScheme.onSecondary), + )) + ], + ), + ), + AdaptiveTextField( + initialValue: formData.auth, + decoration: const InputDecoration( + labelText: 'Auth', + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + enableSuggestions: false, + autocorrect: false, + autofillHints: const [ + AutofillHints.username, + ], + onChanged: (value) { + ref + .read(signInFormProvider.notifier) + .updateWith(auth: value); + }, + ), + AdaptiveTextField( + initialValue: formData.password, + decoration: const InputDecoration( + labelText: 'Password', + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [ + AutofillHints.password, + ], + onChanged: (value) { + ref + .read(signInFormProvider.notifier) + .updateWith(password: value); + }, + ), + Center( + child: AdaptiveButton( + onPressed: () async { + try { + ref + .read(signInFormErrorProvider.notifier) + .updateWith(null); - final result = await api - .getAuthenticationApi() - .authLoginPost(loginBody: formData); - final headers = result.headers; - final cookie = headers.value('set-cookie'); + final result = await api + .getAuthenticationApi() + .authLoginPost(loginBody: formData); + final headers = result.headers; + final cookie = headers.value('set-cookie'); - final resp = await api - .getAuthenticationApi() - .userTokensPost( - headers: {'cookie': cookie}); + final resp = await api + .getAuthenticationApi() + .userTokensPost(headers: {'cookie': cookie}); - final data = resp.data; - if (data != null && data.id != null) { - ref - .read(userTokenProvider.notifier) - .setToken(data.id!); + final data = resp.data; + if (data != null && data.id != null) { + ref + .read(userTokenProvider.notifier) + .setToken(data.id!); - ref - .read(routerProvider) - .go(SplashScreen.path); - } - } catch (e) { - ref - .read(signInFormErrorProvider.notifier) - .updateWith(e); - } - }, - child: Text( - 'Login', - style: theme.textTheme.titleLarge!.copyWith( - color: theme.colorScheme.onPrimary), - )), + ref.read(routerProvider).go(SplashScreen.path); + } + } catch (e) { + ref + .read(signInFormErrorProvider.notifier) + .updateWith(e); + } + }, + child: Text( + 'Login', + style: theme.textTheme.titleLarge! + .copyWith(color: theme.colorScheme.onPrimary), + )), + ), + formError == null + ? Container() + : Container( + margin: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.topLeft, + child: Text( + formError.toString(), + style: theme.textTheme.bodyMedium! + .copyWith(color: theme.colorScheme.error), + ), ), - formError == null - ? Container() - : Container( - margin: - const EdgeInsets.symmetric(vertical: 12), - alignment: Alignment.topLeft, - child: Text( - formError.toString(), - style: theme.textTheme.bodyMedium! - .copyWith(color: theme.colorScheme.error), - ), - ), - ], - ), - )), + ], + ), ), ), ), diff --git a/lib/screens/splash/splash_screen.dart b/lib/screens/splash/splash_screen.dart index 60d0ed4..783ba31 100644 --- a/lib/screens/splash/splash_screen.dart +++ b/lib/screens/splash/splash_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; import 'package:semaphore/router/router.dart'; import 'package:semaphore/screens/home/home_screen.dart'; import 'package:semaphore/screens/sign_in/sign_in_screen.dart'; @@ -38,32 +39,23 @@ class SplashScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; - final size = MediaQuery.of(context).size; final width = size.width > size.height ? size.height : size.width; checkStatus(context, ref); - return Scaffold( - body: NeumorphicBackground( - child: SafeArea( - child: Center( - child: Neumorphic( - style: neumorphicTheme - .getNeumorphicStyle() - .copyWith(boxShape: const NeumorphicBoxShape.circle()), - child: SizedBox( - width: width * 0.8, - height: width * 0.8, - child: Center( - child: CustomPaint( - size: Size(width * 0.7, width * 0.7 * 0.2), - painter: LogoPainter(), - ), - ))), - ), + return AdaptiveScaffold( + body: SafeArea( + child: Center( + child: SizedBox( + width: width * 0.8, + height: width * 0.8, + child: Center( + child: CustomPaint( + size: Size(width * 0.7, width * 0.7 * 0.2), + painter: LogoPainter(), + ), + )), ), ), ); diff --git a/lib/screens/url_config/url_config_screen.dart b/lib/screens/url_config/url_config_screen.dart index c6cd9b4..e7040fb 100644 --- a/lib/screens/url_config/url_config_screen.dart +++ b/lib/screens/url_config/url_config_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; +import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/router/router.dart'; import 'package:semaphore/screens/splash/splash_screen.dart'; import 'package:semaphore/state/api_config.dart'; @@ -13,67 +16,44 @@ class UrlConfigScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final neumorphicTheme = theme.extension()!; final formData = ref.watch(apiUrlProvider); - return Scaffold( - body: NeumorphicBackground( - child: SafeArea( - child: Center( - child: SingleChildScrollView( - child: Neumorphic( - margin: const EdgeInsets.all(24), - padding: const EdgeInsets.all(24), - style: neumorphicTheme.styleWith( - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(24)))), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - child: Text('Semaphore URL', - style: theme.textTheme.titleLarge), - ), - Neumorphic( - margin: const EdgeInsets.symmetric(vertical: 12), - style: neumorphicTheme.styleWith( - depth: -4, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(12)))), - child: TextFormField( - initialValue: formData, - decoration: const InputDecoration( - labelText: 'API URL', - contentPadding: EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - ), - onChanged: (value) { - ref - .read(apiUrlProvider.notifier) - .changeApiUrl(value); - }, - ), - ), - Center( - child: NeumorphicButton( - style: neumorphicTheme.styleWith( - color: theme.colorScheme.primary, - boxShape: NeumorphicBoxShape.roundRect( - const BorderRadius.all(Radius.circular(24))), - ), - onPressed: () { - ref.read(routerProvider).go(SplashScreen.path); - }, - child: Text( - 'Return to Splash Screen', - style: theme.textTheme.titleLarge! - .copyWith(color: theme.colorScheme.onPrimary), - )), - ) - ], - )), + return AdaptiveScaffold( + body: SafeArea( + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: + Text('Semaphore URL', style: theme.textTheme.titleLarge), + ), + AdaptiveTextField( + initialValue: formData, + decoration: const InputDecoration( + labelText: 'API URL', + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + onChanged: (value) { + ref.read(apiUrlProvider.notifier).changeApiUrl(value); + }, + ), + Center( + child: AdaptiveButton( + onPressed: () { + ref.read(routerProvider).go(SplashScreen.path); + }, + child: Text( + 'Return to Splash Screen', + style: theme.textTheme.titleLarge + ?.copyWith(color: theme.colorScheme.onPrimary), + )), + ) + ], ), ), ), diff --git a/lib/state/api_config.dart b/lib/state/api_config.dart index 1ce5b20..4866af7 100644 --- a/lib/state/api_config.dart +++ b/lib/state/api_config.dart @@ -41,15 +41,19 @@ class SemaphoreApi extends _$SemaphoreApi { final apiUrl = ref.read(apiUrlProvider); final token = ref.read(userTokenProvider); ref.keepAlive(); - if (token != null && token.isNotEmpty) { + if (token != null && + token.isNotEmpty && + Uri.parse(apiUrl).host.isNotEmpty) { return AnsibleSemaphore( dio: Dio(BaseOptions( baseUrl: apiUrl, headers: {'Authorization': 'Bearer $token'})), ); - } else { + } else if (Uri.parse(apiUrl).host.isNotEmpty) { return AnsibleSemaphore( basePathOverride: apiUrl, ); + } else { + return AnsibleSemaphore(); } } diff --git a/lib/state/projects.dart b/lib/state/projects.dart index 2994325..25774c7 100644 --- a/lib/state/projects.dart +++ b/lib/state/projects.dart @@ -12,17 +12,19 @@ class Projects extends _$Projects { try { final api = ref.read(semaphoreApiProvider).getProjectsApi(); final resp = await api.projectsGet(); + print('projects resp: $resp'); return resp.data ?? []; } catch (e) { return []; } } - void getProjects() async { + Future getProjects() async { final api = ref.read(semaphoreApiProvider).getProjectsApi(); state = await AsyncValue.guard(() async { try { final resp = await api.projectsGet(); + print('projects resp: $resp'); return resp.data ?? []; } catch (e) { return []; @@ -38,6 +40,7 @@ class CurrentProject extends _$CurrentProject { ref.keepAlive(); try { final projects = await ref.read(projectsProvider.future); + print('projects: $projects'); return projects.first; } catch (e) { return null; diff --git a/lib/state/projects/access_key.dart b/lib/state/projects/access_key.dart index baca424..94b09ef 100644 --- a/lib/state/projects/access_key.dart +++ b/lib/state/projects/access_key.dart @@ -1,8 +1,14 @@ import 'package:ansible_semaphore/ansible_semaphore.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/alert_dialog.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/access_key/form.dart'; import 'package:semaphore/state/api_config.dart'; import 'package:semaphore/state/projects.dart'; @@ -32,58 +38,46 @@ class AccessKeyDataTable extends BaseGridData { return Consumer(builder: (context, ref, _) { return Wrap( children: [ - IconButton( + AdaptiveIconButton( onPressed: () { - showDialog( + adaptiveDialog( context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, - child: AccessKeyForm(accessKeyId: accessKey.id), - ); - }); + child: AccessKeyForm(accessKeyId: accessKey.id)); }, - icon: const Icon(Icons.edit)), - IconButton( + iconData: (Icons.edit)), + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete AccessKey'), - content: const Text( - 'Are you sure you want to delete this accessKey?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - TextButton( - onPressed: () async { - final api = ref - .read(semaphoreApiProvider) - .getProjectApi(); - final current = await ref - .read(currentProjectProvider.future); - await api.projectProjectIdKeysKeyIdDelete( - projectId: current!.id!, - keyId: accessKey.id!); - if (context.mounted) { - Navigator.of(context).pop(); - } - ref - .read(accessKeyListProvider.notifier) - .loadRows(); - }, - child: const Text('Delete')), - ], - ); - }); + adaptiveAlertDialog( + context: context, + title: const Text('Delete AccessKey'), + content: const Text( + 'Are you sure you want to delete this accessKey?'), + primaryButton: AdaptiveButton( + controlSize: ControlSize.large, + onPressed: () async { + final api = + ref.read(semaphoreApiProvider).getProjectApi(); + final current = + await ref.read(currentProjectProvider.future); + await api.projectProjectIdKeysKeyIdDelete( + projectId: current!.id!, keyId: accessKey.id!); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref.read(accessKeyListProvider.notifier).loadRows(); + }, + child: const Text('Delete', + style: TextStyle(color: Colors.red))), + secondaryButton: AdaptiveButton( + controlSize: ControlSize.large, + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ); }, - icon: const Icon(Icons.delete)), + iconData: (Icons.delete)), ], ); }); diff --git a/lib/state/projects/environment.dart b/lib/state/projects/environment.dart index 8781852..aa3aa4e 100644 --- a/lib/state/projects/environment.dart +++ b/lib/state/projects/environment.dart @@ -1,12 +1,17 @@ import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/environment/form.dart'; import 'package:semaphore/state/api_config.dart'; import 'package:semaphore/state/projects.dart'; import 'package:semaphore/utils/base_griddata.dart'; +import 'package:semaphore/adaptive/alert_dialog.dart'; part 'environment.g.dart'; @@ -27,60 +32,51 @@ class EnvironmentDataTable extends BaseGridData { return Consumer(builder: (context, ref, _) { return Wrap( children: [ - IconButton( + AdaptiveIconButton( + onPressed: () { + adaptiveDialog( + context: context, + child: EnvironmentForm(environmentId: environment.id), + ); + }, + iconData: Icons.edit, + ), + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, - child: - EnvironmentForm(environmentId: environment.id), - ); - }); + adaptiveAlertDialog( + context: context, + title: const Text('Delete Environment'), + content: const Text( + 'Are you sure you want to delete this environment?'), + secondaryButton: AdaptiveButton( + controlSize: ControlSize.large, + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + primaryButton: AdaptiveButton( + controlSize: ControlSize.large, + onPressed: () async { + final api = + ref.read(semaphoreApiProvider).getProjectApi(); + final current = + await ref.read(currentProjectProvider.future); + await api + .projectProjectIdEnvironmentEnvironmentIdDelete( + projectId: current!.id!, + environmentId: environment.id!); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(environmentListProvider.notifier) + .loadRows(); + }, + child: const Text('Delete', + style: TextStyle(color: Colors.red))), + ); }, - icon: const Icon(Icons.edit)), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete Environment'), - content: const Text( - 'Are you sure you want to delete this environment?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - TextButton( - onPressed: () async { - final api = ref - .read(semaphoreApiProvider) - .getProjectApi(); - final current = await ref - .read(currentProjectProvider.future); - await api - .projectProjectIdEnvironmentEnvironmentIdDelete( - projectId: current!.id!, - environmentId: environment.id!); - if (context.mounted) { - Navigator.of(context).pop(); - } - ref - .read(environmentListProvider.notifier) - .loadRows(); - }, - child: const Text('Delete')), - ], - ); - }); - }, - icon: const Icon(Icons.delete)), + iconData: Icons.delete), ], ); }); diff --git a/lib/state/projects/inventory.dart b/lib/state/projects/inventory.dart index 0c0f13a..ef210d3 100644 --- a/lib/state/projects/inventory.dart +++ b/lib/state/projects/inventory.dart @@ -1,8 +1,15 @@ +import 'dart:io'; + import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/alert_dialog.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/inventory/form.dart'; import 'package:semaphore/state/api_config.dart'; import 'package:semaphore/state/projects.dart'; @@ -29,6 +36,16 @@ class InventoryDataTable extends BaseGridData { type: PlutoColumnType.text(), renderer: (context) { final Inventory inventory = context.cell.value; + if (Platform.isMacOS) { + return Consumer(builder: (context, ref, _) { + final theme = MacosTheme.of(context); + return Text( + inventory.type == InventoryTypeEnum.file + ? inventory.inventory ?? '' + : '--', + style: theme.typography.body); + }); + } return Text(inventory.type == InventoryTypeEnum.file ? inventory.inventory ?? '' : '--'); @@ -42,59 +59,49 @@ class InventoryDataTable extends BaseGridData { return Consumer(builder: (context, ref, _) { return Wrap( children: [ - IconButton( + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, - child: InventoryForm(inventoryId: inventory.id), - ); - }); + adaptiveDialog( + context: context, + child: InventoryForm(inventoryId: inventory.id), + ); }, - icon: const Icon(Icons.edit)), - IconButton( + iconData: (Icons.edit)), + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete Inventory'), - content: const Text( - 'Are you sure you want to delete this inventory?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - TextButton( - onPressed: () async { - final api = ref - .read(semaphoreApiProvider) - .getProjectApi(); - final current = await ref - .read(currentProjectProvider.future); - await api - .projectProjectIdInventoryInventoryIdDelete( - projectId: current!.id!, - inventoryId: inventory.id!); - if (context.mounted) { - Navigator.of(context).pop(); - } - ref - .read(inventoryListProvider.notifier) - .loadRows(); - }, - child: const Text('Delete')), - ], - ); - }); + adaptiveAlertDialog( + context: context, + title: const Text('Delete Inventory'), + content: const Text( + 'Are you sure you want to delete this inventory?'), + secondaryButton: AdaptiveButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + primaryButton: AdaptiveButton( + onPressed: () async { + final api = ref + .read(semaphoreApiProvider) + .getProjectApi(); + final current = + await ref.read(currentProjectProvider.future); + await api + .projectProjectIdInventoryInventoryIdDelete( + projectId: current!.id!, + inventoryId: inventory.id!); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(inventoryListProvider.notifier) + .loadRows(); + }, + child: const Text('Delete', + style: TextStyle(color: Colors.red))), + ); }, - icon: const Icon(Icons.delete)), + iconData: (Icons.delete)), ], ); }); diff --git a/lib/state/projects/repository.dart b/lib/state/projects/repository.dart index 5235220..b97e77a 100644 --- a/lib/state/projects/repository.dart +++ b/lib/state/projects/repository.dart @@ -3,6 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/alert_dialog.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/access_key_name.dart'; import 'package:semaphore/components/repository/form.dart'; import 'package:semaphore/state/api_config.dart'; @@ -42,59 +46,48 @@ class RepositoryDataTable extends BaseGridData { return Consumer(builder: (context, ref, _) { return Wrap( children: [ - IconButton( + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, - child: RepositoryForm(repositoryId: repository.id), - ); - }); + adaptiveDialog( + context: context, + child: RepositoryForm(repositoryId: repository.id), + ); }, - icon: const Icon(Icons.edit)), - IconButton( + iconData: (Icons.edit)), + AdaptiveIconButton( onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete Repository'), - content: const Text( - 'Are you sure you want to delete this repository?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel')), - TextButton( - onPressed: () async { - final api = ref - .read(semaphoreApiProvider) - .getProjectApi(); - final current = await ref - .read(currentProjectProvider.future); - await api - .projectProjectIdRepositoriesRepositoryIdDelete( - projectId: current!.id!, - repositoryId: repository.id!); - if (context.mounted) { - Navigator.of(context).pop(); - } - ref - .read(repositoryListProvider.notifier) - .loadRows(); - }, - child: const Text('Delete')), - ], - ); - }); + adaptiveAlertDialog( + context: context, + title: const Text('Delete Repository'), + content: const Text( + 'Are you sure you want to delete this repository?'), + secondaryButton: AdaptiveButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + primaryButton: AdaptiveButton( + onPressed: () async { + final api = + ref.read(semaphoreApiProvider).getProjectApi(); + final current = + await ref.read(currentProjectProvider.future); + await api + .projectProjectIdRepositoriesRepositoryIdDelete( + projectId: current!.id!, + repositoryId: repository.id!); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(repositoryListProvider.notifier) + .loadRows(); + }, + child: const Text('Delete', + style: TextStyle(color: Colors.red))), + ); }, - icon: const Icon(Icons.delete)), + iconData: (Icons.delete)), ], ); }); diff --git a/lib/state/projects/task.dart b/lib/state/projects/task.dart index 428d2f7..e66a496 100644 --- a/lib/state/projects/task.dart +++ b/lib/state/projects/task.dart @@ -5,13 +5,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/text_duration.dart'; +import 'package:semaphore/adaptive/text_null.dart'; +import 'package:semaphore/adaptive/text_timeago.dart'; import 'package:semaphore/components/status_chip.dart'; import 'package:semaphore/components/task_output_view.dart'; import 'package:semaphore/components/template_link.dart'; import 'package:semaphore/state/api_config.dart'; import 'package:semaphore/state/projects.dart'; import 'package:semaphore/utils/base_griddata.dart'; -import 'package:timeago/timeago.dart' as timeago; +import 'package:semaphore/adaptive/dialog.dart'; part 'task.g.dart'; @@ -38,7 +42,7 @@ class TaskDataTable extends BaseGridData { }, ); }), - const Icon(Icons.keyboard_arrow_left), + const AdaptiveIcon(Icons.keyboard_arrow_left), TemplateLink(templateId: renderContext.cell.value.templateId), ]); }, @@ -49,8 +53,7 @@ class TaskDataTable extends BaseGridData { type: PlutoColumnType.text(), renderer: (PlutoColumnRendererContext renderContext) { final String? value = renderContext.cell.value; - if (value == 'null') return const Text('--'); - return Text(value ?? '--'); + return AdaptiveTextNull(value); }, ), PlutoColumn( @@ -73,10 +76,7 @@ class TaskDataTable extends BaseGridData { type: PlutoColumnType.text(), renderer: (PlutoColumnRendererContext renderContext) { final DateTime? value = renderContext.cell.value; - if (value == null) { - return const Text('--'); - } - return Text(timeago.format(value)); + return AdaptiveTextTimeago(value); }, ), PlutoColumn( @@ -85,13 +85,7 @@ class TaskDataTable extends BaseGridData { type: PlutoColumnType.text(), renderer: (PlutoColumnRendererContext renderContext) { final [start, end] = renderContext.cell.value; - final DateTime endN = end ?? DateTime.now(); - if (start == null) { - return const Text('--'); - } - final DateTime startN = start; - final d = endN.difference(startN); - return Text('${d.inSeconds} ${d.inSeconds > 1 ? "seconds" : "second"}'); + return AdaptiveTextDuration(start, end); }, ), ]; @@ -174,16 +168,12 @@ class TaskList extends _$TaskList { } showOutput(BuildContext context, int id) { - showDialog( - context: context, - builder: (context) { - return Dialog.fullscreen( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - child: TaskOutputView( - id: id, - ), - ); - }); + adaptiveDialog( + context: context, + child: TaskOutputView( + id: id, + ), + ); } } diff --git a/lib/state/projects/template.dart b/lib/state/projects/template.dart index 156e5fa..72abbea 100644 --- a/lib/state/projects/template.dart +++ b/lib/state/projects/template.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/environment_name.dart'; import 'package:semaphore/components/inventory_name.dart'; import 'package:semaphore/components/repository.dart'; @@ -78,15 +79,14 @@ class TemplateDataTable extends BaseGridData