diff --git a/LICENSE b/LICENSE index fd7f577..3564989 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2022 Jonathan Gao +Copyright (c) 2023 Jonathan Gao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 751fe5f..263429e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Run ansible task from your phone. ## Getting Started -Download from +Download from - [AppStore]() - [PlayStore](https://play.google.com/store/apps/details?id=org.gsmlg.semaphore) @@ -12,6 +12,7 @@ Download from ## Screens -![](docs/screens/drawer.png) -![](docs/screens/template.png) -![](docs/screens/task_output.png) +Light Mode | Dark Mode +:-------------------------:|:-------------------------: +![](fastlane/metadata/android/en-US/images/phoneScreenshots/1.png) | ![](fastlane/metadata/android/en-US/images/phoneScreenshots/2.png) +![](fastlane/metadata/android/en-US/images/phoneScreenshots/4.png) | ![](fastlane/metadata/android/en-US/images/phoneScreenshots/3.png) diff --git a/lib/adaptive/alert_dialog.dart b/lib/adaptive/alert_dialog.dart index 9395f73..92d1128 100644 --- a/lib/adaptive/alert_dialog.dart +++ b/lib/adaptive/alert_dialog.dart @@ -21,12 +21,14 @@ adaptiveAlertDialog({ message: content, //horizontalActions: false, primaryButton: PushButton( + color: primaryButton.color, controlSize: ControlSize.large, onPressed: primaryButton.onPressed, child: primaryButton.child, ), secondaryButton: secondaryButton != null ? PushButton( + color: secondaryButton.color, secondary: true, controlSize: ControlSize.large, onPressed: secondaryButton.onPressed, @@ -45,16 +47,25 @@ adaptiveAlertDialog({ actions: secondaryButton == null ? [ TextButton( + style: ButtonStyle( + textStyle: MaterialStateProperty.all( + TextStyle(color: primaryButton.color))), onPressed: primaryButton.onPressed, child: primaryButton.child, ), ] : [ TextButton( + style: ButtonStyle( + textStyle: MaterialStateProperty.all( + TextStyle(color: secondaryButton.color))), onPressed: secondaryButton.onPressed, child: secondaryButton.child, ), TextButton( + style: ButtonStyle( + textStyle: MaterialStateProperty.all( + TextStyle(color: primaryButton.color))), onPressed: primaryButton.onPressed, child: primaryButton.child, ), diff --git a/lib/adaptive/app.dart b/lib/adaptive/app.dart index 30355ff..724152b 100644 --- a/lib/adaptive/app.dart +++ b/lib/adaptive/app.dart @@ -10,7 +10,7 @@ class AdaptiveApp extends StatelessWidget { final String title; final RouterConfig? routerConfig; final ThemeMode themeMode; - final AppThemeData appThemeData; + final Color accentColor; const AdaptiveApp({ super.key, @@ -18,7 +18,7 @@ class AdaptiveApp extends StatelessWidget { this.debugShowCheckedModeBanner = false, this.routerConfig, this.themeMode = ThemeMode.system, - required this.appThemeData, + required this.accentColor, }); const AdaptiveApp.router({ @@ -27,12 +27,17 @@ class AdaptiveApp extends StatelessWidget { this.debugShowCheckedModeBanner = false, this.routerConfig, this.themeMode = ThemeMode.system, - required this.appThemeData, + required this.accentColor, }); @override Widget build(BuildContext context) { if (Platform.isMacOS) { + final lightTheme = MacosThemeData.light().copyWith( + primaryColor: accentColor, + ); + final darkTheme = + MacosThemeData.dark().copyWith(primaryColor: accentColor); return MacosApp.router( localizationsDelegates: const >[ DefaultMaterialLocalizations.delegate, @@ -42,11 +47,12 @@ class AdaptiveApp extends StatelessWidget { routerConfig: routerConfig, title: title, themeMode: themeMode, - // theme: MacosThemeData.light(), - darkTheme: MacosThemeData.dark(), + theme: lightTheme, + darkTheme: darkTheme, ); } + final appThemeData = AppThemeData.fromSeed(accentColor); return MaterialApp.router( debugShowCheckedModeBanner: debugShowCheckedModeBanner, routerConfig: routerConfig, diff --git a/lib/adaptive/button.dart b/lib/adaptive/button.dart index 7a91739..6d53134 100644 --- a/lib/adaptive/button.dart +++ b/lib/adaptive/button.dart @@ -4,16 +4,18 @@ 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; + show NeumorphicButton, NeumorphicTheme; class AdaptiveButton extends StatelessWidget { final Function()? onPressed; final Widget child; final ControlSize controlSize; + final Color? color; const AdaptiveButton({ super.key, this.onPressed, + this.color, this.child = const SizedBox(), this.controlSize = ControlSize.regular, }); @@ -21,10 +23,17 @@ class AdaptiveButton extends StatelessWidget { @override Widget build(BuildContext context) { if (Platform.isMacOS) { + // final macosTheme = MacosTheme.of(context); return PushButton( - controlSize: controlSize, onPressed: onPressed, child: child); + color: color, + controlSize: controlSize, + onPressed: onPressed, + child: child); } + final theme = Theme.of(context); + final neumorphicTheme = theme.extension()!; return NeumorphicButton( + style: neumorphicTheme.styleWith(color: color), onPressed: onPressed, child: child, ); diff --git a/lib/adaptive/dropdown.dart b/lib/adaptive/dropdown.dart index 1da49dc..d1dabd4 100644 --- a/lib/adaptive/dropdown.dart +++ b/lib/adaptive/dropdown.dart @@ -26,7 +26,10 @@ class AdaptiveDropdownMenu extends StatelessWidget { children: [ decoration?.labelText != null ? Text(decoration!.labelText!, style: theme.typography.headline) - : Container(), + : const SizedBox(), + decoration?.labelText != null + ? const SizedBox(height: 12.0) + : const SizedBox(), MacosPopupButton( key: key, value: value, diff --git a/lib/adaptive/floatingAction.dart b/lib/adaptive/floatingAction.dart index 4f57bff..7afd9ed 100644 --- a/lib/adaptive/floatingAction.dart +++ b/lib/adaptive/floatingAction.dart @@ -28,6 +28,7 @@ class AdaptiveFloatingAction { Widget floatingActionButton() { return NeumorphicFloatingActionButton( + tooltip: label, onPressed: onPressed, child: icon, ); diff --git a/lib/adaptive/icon_button.dart b/lib/adaptive/icon_button.dart index 3bc8bc9..7082010 100644 --- a/lib/adaptive/icon_button.dart +++ b/lib/adaptive/icon_button.dart @@ -1,27 +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 AdaptiveIconButton extends StatelessWidget { final Function()? onPressed; - final IconData? iconData; + final Widget icon; + final String? label; const AdaptiveIconButton({ super.key, this.onPressed, - this.iconData, + this.label, + required this.icon, }); @override Widget build(BuildContext context) { if (Platform.isMacOS) { - return MacosIconButton(onPressed: onPressed, icon: MacosIcon(iconData)); + return MacosIconButton( + semanticLabel: label, + onPressed: onPressed, + icon: icon, + ); } return IconButton( + tooltip: label, onPressed: onPressed, - icon: Icon(iconData), + icon: icon, ); } } diff --git a/lib/adaptive/scaffold.dart b/lib/adaptive/scaffold.dart index b2efce1..4f5034f 100644 --- a/lib/adaptive/scaffold.dart +++ b/lib/adaptive/scaffold.dart @@ -30,17 +30,31 @@ class AdaptiveScaffold extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { if (Platform.isMacOS) { final toolbarAction = floatingAction?.toolBarIconButton(); + final dynamic extraActions = appBar?.actions ?? []; + final toolbarExtraActions = extraActions + .map((a) => ToolBarIconButton( + icon: a.icon ?? const SizedBox(), + onPressed: a.onPressed, + showLabel: a.showLabel ?? false, + label: a.label, + )) + .toList(); return MacosWindow( titleBar: appBar?.title != null ? TitleBar(title: Text(appBar!.title)) : null, - sidebar: buildSidebar(context), + sidebar: drawer != null ? buildSidebar(context) : null, child: MacosScaffold( backgroundColor: const Color.fromRGBO(0xff, 0xff, 0xff, 0), - toolBar: toolbarAction != null - ? ToolBar(actions: [ - toolbarAction, - ]) + toolBar: toolbarAction != null || appBar?.leading != null + ? ToolBar( + leading: appBar?.leading, + actions: toolbarAction != null + ? [ + toolbarAction, + ...toolbarExtraActions, + ] + : toolbarExtraActions) : null, children: [ ContentArea(builder: (context, scrollController) { diff --git a/lib/adaptive/switch.dart b/lib/adaptive/switch.dart new file mode 100644 index 0000000..76fa6f2 --- /dev/null +++ b/lib/adaptive/switch.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 AdaptiveSwitch extends StatelessWidget { + final void Function(bool)? onChanged; + final bool value; + + const AdaptiveSwitch({ + super.key, + this.onChanged, + this.value = false, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosSwitch(onChanged: onChanged, value: value); + } + return NeumorphicSwitch( + value: value, + onChanged: onChanged ?? (value) {}, + ); + } +} diff --git a/lib/adaptive/tab_view.dart b/lib/adaptive/tab_view.dart new file mode 100644 index 0000000..a3849e0 --- /dev/null +++ b/lib/adaptive/tab_view.dart @@ -0,0 +1,71 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveTabView extends StatefulWidget { + final List tabs; + final List children; + final int initialIndex; + + const AdaptiveTabView({ + super.key, + required this.tabs, + required this.children, + this.initialIndex = 0, + }); + + @override + State createState() => _AdaptiveTabViewState(); +} + +class _AdaptiveTabViewState extends State + with TickerProviderStateMixin { + late TabController _tabController; + late MacosTabController _macosTabController; + + @override + initState() { + super.initState(); + final length = widget.tabs.length; + _tabController = TabController( + length: length, vsync: this, initialIndex: widget.initialIndex); + _macosTabController = + MacosTabController(length: length, initialIndex: widget.initialIndex); + } + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + return MacosTabView( + controller: _macosTabController, + tabs: widget.tabs.map((tab) => MacosTab(label: tab)).toList(), + children: widget.children, + ); + } + final theme = Theme.of(context); + return Column( + children: [ + Container( + color: theme.colorScheme.primary, + child: TabBar( + controller: _tabController, + tabs: widget.tabs + .map((tab) => Tab( + child: Text(tab, + style: const TextStyle() + .copyWith(color: theme.colorScheme.onPrimary)))) + .toList(), + ), + ), + Flexible( + child: TabBarView( + clipBehavior: Clip.none, + controller: _tabController, + children: widget.children, + ), + ) + ], + ); + } +} diff --git a/lib/adaptive/text.dart b/lib/adaptive/text.dart new file mode 100644 index 0000000..cb3d2c1 --- /dev/null +++ b/lib/adaptive/text.dart @@ -0,0 +1,70 @@ +import 'dart:io' show Platform; + +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class AdaptiveTextBody extends StatelessWidget { + final String data; + + const AdaptiveTextBody( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Text(data, + style: + theme.typography.body.copyWith(overflow: TextOverflow.ellipsis)); + } + final theme = Theme.of(context); + return Text( + data, + style: + theme.textTheme.bodyMedium?.copyWith(overflow: TextOverflow.ellipsis), + ); + } +} + +class AdaptiveTextTitle extends StatelessWidget { + final String data; + + const AdaptiveTextTitle( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Text(data, style: theme.typography.title2); + } + final theme = Theme.of(context); + return Text(data, style: theme.textTheme.titleMedium); + } +} + +class AdaptiveTextError extends StatelessWidget { + final String data; + + const AdaptiveTextError( + this.data, { + super.key, + }); + + @override + Widget build(BuildContext context) { + if (Platform.isMacOS) { + final theme = MacosTheme.of(context); + return Text(data, + style: theme.typography.body.copyWith(color: Colors.redAccent)); + } + final theme = Theme.of(context); + return Text(data, + softWrap: true, + style: theme.textTheme.bodyMedium?.copyWith(color: Colors.redAccent)); + } +} diff --git a/lib/adaptive/text_field.dart b/lib/adaptive/text_field.dart index a5505ae..1e291cd 100644 --- a/lib/adaptive/text_field.dart +++ b/lib/adaptive/text_field.dart @@ -38,7 +38,10 @@ class AdaptiveTextField extends StatelessWidget { children: [ decoration?.labelText != null ? Text(decoration!.labelText!, style: theme.typography.headline) - : Container(), + : const SizedBox(), + decoration?.labelText != null + ? const SizedBox(height: 12.0) + : const SizedBox(), MacosTextFormField( initialValue: initialValue, obscureText: obscureText, diff --git a/lib/app.dart b/lib/app.dart index 2f76e81..4d2935b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -3,6 +3,8 @@ 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/database/database.dart'; +import 'package:semaphore/database/schema/app_activities.dart'; import 'package:semaphore/router/router.dart'; import 'package:semaphore/state/theme.dart'; import 'package:semaphore/adaptive/app.dart'; @@ -16,17 +18,36 @@ class App extends ConsumerStatefulWidget { ConsumerState createState() => _AppState(); } -class _AppState extends ConsumerState { +class _AppState extends ConsumerState with WidgetsBindingObserver { @override initState() { super.initState(); + ref + .read(localAppThemeDataProvider.notifier) + .saveSeedColor(SystemTheme.accentColor.accent); if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { initSystemTray(); } + WidgetsBinding.instance.addObserver(this); + if (WidgetsBinding.instance.lifecycleState != null) { + print(WidgetsBinding.instance.lifecycleState!); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + final appActivity = AppActivities() + ..state = state + ..createdAt = DateTime.now(); + + await Database().instance.writeTxn(() async { + await Database().instance.appActivities.put(appActivity); + }); } @override void dispose() { + WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -69,15 +90,14 @@ class _AppState extends ConsumerState { 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 AdaptiveApp.router( + accentColor: accentColor, debugShowCheckedModeBanner: false, routerConfig: router, title: Constants.appName, - // themeMode: ThemeMode.system, - appThemeData: appThemeData, + themeMode: themeMode, ); } } diff --git a/lib/components/app_bar.dart b/lib/components/app_bar.dart index 98a1c9b..c3935ba 100644 --- a/lib/components/app_bar.dart +++ b/lib/components/app_bar.dart @@ -5,12 +5,16 @@ import 'package:material_neumorphic/material_neumorphic.dart'; class LocalAppBar extends ConsumerWidget implements PreferredSizeWidget { final String title; final List actions; + final Widget? leading; @override final Size preferredSize; const LocalAppBar( - {Key? key, this.title = 'Ansible Semaphore', this.actions = const []}) + {Key? key, + this.title = 'Ansible Semaphore', + this.actions = const [], + this.leading}) : preferredSize = const Size.fromHeight(NeumorphicAppBar.toolbarHeight), super(key: key); @@ -28,12 +32,13 @@ class LocalAppBar extends ConsumerWidget implements PreferredSizeWidget { style: theme.textTheme.headlineMedium! .copyWith(color: theme.colorScheme.onPrimary), ), - leading: Builder( - builder: (context) => NeumorphicButton( - style: style.copyWith(color: theme.colorScheme.primary), - child: const Icon(Icons.menu), - onPressed: () => Scaffold.of(context).openDrawer()), - ), + leading: leading ?? + Builder( + builder: (context) => NeumorphicButton( + style: style.copyWith(color: theme.colorScheme.primary), + child: const Icon(Icons.menu), + onPressed: () => Scaffold.of(context).openDrawer()), + ), actionSpacing: 16, actions: actions, ); diff --git a/lib/components/app_drawer.dart b/lib/components/app_drawer.dart index 0eabd71..b55fcba 100644 --- a/lib/components/app_drawer.dart +++ b/lib/components/app_drawer.dart @@ -3,13 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:material_neumorphic/material_neumorphic.dart'; import 'package:semaphore/router/router.dart'; +import 'package:semaphore/screens/profile/profile_screen.dart'; import 'package:semaphore/screens/project/environment_screen.dart'; -import 'package:semaphore/screens/project/history_screen.dart'; +import 'package:semaphore/screens/project/dashboard_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/setting/setting_screen.dart'; +import 'package:semaphore/state/auth.dart'; import 'package:semaphore/state/projects.dart'; import 'package:semaphore/state/theme.dart'; @@ -30,128 +33,156 @@ class LocalDrawer extends ConsumerWidget { final projects = ref.watch(projectsProvider); final currentProject = ref.watch(currentProjectProvider); + final user = ref.watch(currentUserProvider); + return Drawer( - 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: NeumorphicBackground( + child: Column( + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: theme.colorScheme.primary, ), - 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}' - }); - }, - ), - 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}' - }); - }, + margin: const EdgeInsets.only(bottom: 1.0), + padding: const EdgeInsets.fromLTRB(16.0, 2.0, 16.0, 1.0), + child: Center( + child: ListTile( + leading: + Icon(Icons.rocket, color: theme.colorScheme.onPrimary), + title: Text( + 'Project', + style: theme.textTheme.titleMedium + ?.copyWith(color: theme.colorScheme.onPrimary), + ), + subtitle: currentProject.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const NeumorphicProgressIndeterminate(), + data: (current) { + return Text( + current?.name ?? '--', + style: theme.textTheme.titleLarge?.copyWith( + color: + theme.colorScheme.onPrimary.withOpacity(0.7)), + ); + }), ), + ), + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + ListTile( + leading: const Icon(Icons.dashboard), + title: const Text('Dashboard'), + selected: false, + onTap: () { + router.goNamed(DashboardScreen.name, pathParameters: { + 'pid': '${currentProject.value?.id ?? 1}' + }); + }, + ), + 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}' + }); + }, + ), + ], + ), + ), + SafeArea( + child: Column( + children: [ + const Divider(), ListTile( - leading: const Icon(Icons.gite), - title: const Text('Repositories'), + leading: const Icon(Icons.settings), + title: const Text('Setting'), + subtitle: const Text('Theming, Server or Project'), selected: false, onTap: () { - router.goNamed(RepositoryScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' + router.goNamed(SettingsScreen.name, pathParameters: { + 'module': SettingsScreenModule.theme.name }); }, ), ListTile( - leading: const Icon(Icons.group_work), - title: const Text('Team'), + leading: const Icon(Icons.account_box), + title: const Text('Profile'), + subtitle: user.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const LinearProgressIndicator(), + data: (user) => Text(user?.name ?? '--'), + ), selected: false, onTap: () { - router.goNamed(TeamScreen.name, pathParameters: { - 'pid': '${currentProject.value?.id ?? 1}' + router.goNamed(ProfileScreen.name, pathParameters: { + 'module': ProfileScreenModule.info.name }); }, ), ], - ), - ), - ], + )), + ], + ), ), ); } diff --git a/lib/components/inventory/form.dart b/lib/components/inventory/form.dart index 3ce0ad9..7fca0a8 100644 --- a/lib/components/inventory/form.dart +++ b/lib/components/inventory/form.dart @@ -4,9 +4,9 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/access_key.dart'; @@ -95,7 +95,7 @@ class InventoryForm extends ConsumerWidget { suffixIcon: formData.becomeKeyId == null ? null : AdaptiveIconButton( - iconData: (Icons.clear), + icon: const AdaptiveIcon(Icons.clear), onPressed: () { ref .read(inventoryFormRequestProvider( @@ -172,8 +172,9 @@ class InventoryForm extends ConsumerWidget { inventory) .notifier) .postInventory(); - if (context.mounted) + if (context.mounted) { Navigator.of(context).pop(); + } ref .read(inventoryListProvider.notifier) .loadRows(); diff --git a/lib/components/macos/sidebar.dart b/lib/components/macos/sidebar.dart index 3f2ebe8..dcbef13 100644 --- a/lib/components/macos/sidebar.dart +++ b/lib/components/macos/sidebar.dart @@ -1,167 +1,202 @@ -import 'package:ansible_semaphore/ansible_semaphore.dart'; +import 'package:flutter/cupertino.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/profile/profile_screen.dart'; import 'package:semaphore/screens/project/environment_screen.dart'; -import 'package:semaphore/screens/project/history_screen.dart'; +import 'package:semaphore/screens/project/dashboard_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/screens/setting/setting_screen.dart'; +import 'package:semaphore/state/auth.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); + minWidth: 220, + top: MacosListTile( + leading: const MacosIcon( + CupertinoIcons.rocket, + color: MacosColors.systemGrayColor, + ), + title: Text( + 'Project', + style: MacosTypography.of(context).title2, + ), + subtitle: Consumer(builder: (context, ref, _) { + final currentProject = ref.watch(currentProjectProvider); - return projects.when( - data: (data) { - return currentProject.when( - error: (error, stackTrace) => SizedBox( - child: Text(error.toString()), + return currentProject.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const ProgressCircle(), + data: (current) { + return Text( + current?.name ?? '--', + style: MacosTypography.of(context) + .title2 + .copyWith(color: MacosColors.systemGrayColor), + ); + }); + }), + ), + 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(DashboardScreen.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'), + ), + ], + ); + }); + }, + bottom: Consumer(builder: (context, ref, _) { + final router = ref.watch(routerProvider); + final user = ref.watch(currentUserProvider); + + return user.when( + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + loading: () => const ProgressCircle(), + data: (user) { + return Column( + children: [ + MacosListTile( + leading: const MacosIcon( + CupertinoIcons.settings, + color: MacosColors.systemGrayColor, ), - loading: () => const ProgressCircle(), - data: (current) { - return MacosPopupButton( - value: current, - onChanged: (Project? value) { - ref - .read(currentProjectProvider.notifier) - .setCurrent(value); + title: const Text('Setting'), + subtitle: const Text('Theming, Server or Project'), + onClick: () { + router.goNamed(SettingsScreen.name, pathParameters: { + 'module': SettingsScreenModule.theme.name + }); }, - 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'), - ), - ], - ); - }); - }, - ); + ), + const SizedBox( + height: 8.0, + ), + MacosListTile( + leading: const MacosIcon( + Icons.person, + color: MacosColors.systemGrayColor, + ), + title: const Text('Profile'), + subtitle: Text( + user?.name ?? '--', + ), + onClick: () { + router.goNamed(ProfileScreen.name, pathParameters: { + 'module': ProfileScreenModule.info.name + }); + }, + ), + ], + ); + }); + })); } diff --git a/lib/components/project/form.dart b/lib/components/project/form.dart new file mode 100644 index 0000000..2961ae7 --- /dev/null +++ b/lib/components/project/form.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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.dart'; +import 'package:semaphore/adaptive/text_field.dart'; +import 'package:semaphore/state/projects.dart'; + +class ProjectForm extends ConsumerWidget { + final int? projectId; + const ProjectForm({super.key, this.projectId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); + final project = ref.watch(projectFamilyProvider(projectId)).value; + final formData = ref.watch(projectFormStateProvider(project)); + + 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: const AdaptiveTextTitle('New Project'), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { + ref + .read(projectFormStateProvider(project).notifier) + .updateWith(name: value); + }, + decoration: const InputDecoration( + labelText: 'Project Name', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox( + height: 24.0, + ), + Row( + children: [ + AdaptiveCheckbox( + value: formData.alert, + onChanged: (bool value) { + ref + .read(projectFormStateProvider(project).notifier) + .updateWith(alert: value); + }, + ), + const SizedBox( + width: 12.0, + ), + const AdaptiveTextBody('Allow alerts for this project'), + ], + ), + const SizedBox( + height: 24.0, + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.alertChat, + onChanged: (value) { + ref + .read(projectFormStateProvider(project).notifier) + .updateWith(alertChat: value); + }, + decoration: const InputDecoration( + labelText: 'Telegram Chat ID (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox( + height: 24.0, + ), + AdaptiveTextField( + autocorrect: false, + // ignore: prefer_null_aware_operators + initialValue: formData.maxParallelTasks == null + ? null + : formData.maxParallelTasks.toString(), + onChanged: (value) { + ref + .read(projectFormStateProvider(project).notifier) + .updateWith(maxParallelTasks: int.tryParse(value) ?? 0); + }, + decoration: const InputDecoration( + labelText: 'Max number of parallel tasks (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + AdaptiveButton( + onPressed: () async { + await ref + .read(projectsProvider.notifier) + .createProject(formData); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Save'), + ), + AdaptiveTextButton( + onPressed: () async { + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Cancel'), + ), + ], + ), + const SizedBox(height: 24), + ]), + ), + ), + ); + } +} diff --git a/lib/components/repository/form.dart b/lib/components/repository/form.dart index fccd6dc..0850b7c 100644 --- a/lib/components/repository/form.dart +++ b/lib/components/repository/form.dart @@ -7,6 +7,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/adaptive/text_field.dart'; import 'package:semaphore/state/projects/access_key.dart'; @@ -96,7 +97,7 @@ class RepositoryForm extends ConsumerWidget { suffixIcon: formData.sshKeyId == null ? null : AdaptiveIconButton( - iconData: (Icons.clear), + icon: const AdaptiveIcon(Icons.clear), onPressed: () { ref .read(repositoryFormRequestProvider( diff --git a/lib/components/server/form.dart b/lib/components/server/form.dart new file mode 100644 index 0000000..29f1fd3 --- /dev/null +++ b/lib/components/server/form.dart @@ -0,0 +1,163 @@ +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/adaptive/button.dart'; +import 'package:semaphore/adaptive/switch.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/adaptive/text_field.dart'; +import 'package:semaphore/state/server.dart'; +import 'package:semaphore/state/user.dart'; + +class ServerForm extends ConsumerWidget { + final int? serverId; + const ServerForm({super.key, this.serverId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); + final server = ref.watch(serverFamilyProvider(serverId)); + final formData = ref.watch(serverFormStateProvider(server)); + + final dataList = [ + formData.name, + formData.apiUrl, + formData.username, + formData.password + ]; + final canntGetToken = (dataList.contains('') || dataList.contains(null)); + + 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: AdaptiveTextTitle( + server?.id == null ? 'New Server' : 'Edit Server', + ), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { + ref + .read(serverFormStateProvider(server).notifier) + .updateWith(name: value); + }, + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.apiUrl, + onChanged: (value) { + ref + .read(serverFormStateProvider(server).notifier) + .updateWith(apiUrl: value); + }, + decoration: const InputDecoration( + labelText: 'API URL', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.username, + onChanged: (value) { + ref + .read(serverFormStateProvider(server).notifier) + .updateWith(username: value); + }, + decoration: const InputDecoration( + labelText: 'Username', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + obscureText: true, + onChanged: (value) { + ref + .read(serverFormStateProvider(server).notifier) + .updateWith(password: value); + }, + decoration: const InputDecoration( + labelText: 'Password', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + Row( + children: [ + AdaptiveButton( + onPressed: canntGetToken + ? null + : () async { + // print(dataList); + // print(canntGetToken); + // return; + if (canntGetToken) { + return; + } + final token = await ref + .read(serverFormStateProvider(server).notifier) + .getTokenFromServer(); + if (token != null) { + ref + .read(serverFormStateProvider(server).notifier) + .updateWith(token: token); + } else { + print('error get token'); + } + }, + child: const Text('Get Token from Server'), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + const AdaptiveTextBody('Token: '), + const SizedBox(width: 24), + Expanded(child: AdaptiveTextBody(formData.token ?? '--')), + ], + ), + const SizedBox(height: 24), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + AdaptiveButton( + onPressed: formData.token == null || formData.token == '' + ? null + : () { + ref + .read(serversProvider.notifier) + .putServer(formData); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Save Server'), + ), + AdaptiveTextButton( + onPressed: () async { + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Cancel'), + ), + ], + ), + const SizedBox(height: 24), + ]), + ), + ), + ); + } +} diff --git a/lib/components/task_output_view.dart b/lib/components/task_output_view.dart index 51767ac..e5a334b 100644 --- a/lib/components/task_output_view.dart +++ b/lib/components/task_output_view.dart @@ -5,6 +5,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/status_chip.dart'; import 'package:semaphore/state/projects/task.dart'; @@ -18,7 +19,6 @@ class TaskOutputView extends ConsumerWidget { 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; @@ -33,84 +33,75 @@ class TaskOutputView extends ConsumerWidget { isDark = macosTheme.brightness.isDark; } - return task.when( - data: (task) { - return Container( - color: bgColor, - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - Text( - 'Task #${task.id}', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(color: textColor), + return Container( + color: bgColor, + padding: const EdgeInsets.all(16), + child: taskOutput.when( + data: ((Task, List) args) { + var (task, taskOutput) = args; + print(task); + return Column( + children: [ + Row( + children: [ + Text( + 'Task #${task.id}', + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(color: textColor), + ), + const SizedBox(width: 16), + Material( + color: bgColor, + child: StatusChip(status: task.status), + ), + const Spacer(), + AdaptiveIconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const AdaptiveIcon(Icons.close)) + ], + ), + const SizedBox(height: 12), + Expanded( + child: 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: GoogleFonts.robotoMono( + textStyle: TextStyle(color: textColor), + ), + ), + ]))) + .toList(), + ), ), - const SizedBox(width: 16), - Material( - color: bgColor, - child: StatusChip(status: task.status), - ), - const Spacer(), - AdaptiveIconButton( - onPressed: () { - Navigator.of(context).pop(); - }, - iconData: (Icons.close)) - ], - ), - const SizedBox(height: 12), - Expanded( - child: taskOutput.when( - 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: GoogleFonts.robotoMono( - textStyle: - TextStyle(color: textColor), - ), - ), - ]))) - .toList(), - ), - ), - loading: () => Center( - child: Platform.isMacOS - ? const ProgressCircle() - : const CircularProgressIndicator()), - error: (error, stackTrace) => const Text('N/A')), - ), - ], - ), - ); - }, - loading: () => Center( - child: Platform.isMacOS - ? const ProgressCircle() - : const CircularProgressIndicator()), - error: (error, stackTrace) => const Text('N/A'), - ); + ), + ], + ); + }, + loading: () => Center( + child: Platform.isMacOS + ? const ProgressCircle() + : const CircularProgressIndicator()), + error: (error, stackTrace) => const Text('N/A'), + )); } } diff --git a/lib/components/user/form.dart b/lib/components/user/form.dart index e69de29..3b31fcd 100644 --- a/lib/components/user/form.dart +++ b/lib/components/user/form.dart @@ -0,0 +1,141 @@ +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:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dropdown.dart'; +import 'package:semaphore/state/projects/user.dart'; +import 'package:semaphore/state/user.dart'; + +class ProjectUserForm extends ConsumerWidget { + final int? userId; + const ProjectUserForm({super.key, this.userId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); + final user = ref.watch(userFamily(userId)); + final formData = ref.watch(ProjectUserFormRequestProvider(user.value)); + final systemUser = ref.watch(systemUserProvider); + + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + + return user.when( + data: (user) { + 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( + user.id == null + ? 'New Project User' + : 'Edit Project User', + style: textTitleStyle), + ), + systemUser.when( + data: (data) { + return AdaptiveDropdownMenu( + decoration: const InputDecoration( + labelText: 'User', + contentPadding: EdgeInsets.all(8), + ), + value: formData.userId, + onChanged: (int? value) { + ref + .read(projectUserFormRequestProvider(user) + .notifier) + .updateWith(userId: value); + }, + items: data.map>( + (User value) { + return AdaptiveDropdownMenuItem( + value: value.id, + child: Text(value.name ?? '--'), + ); + }).toList(), + ); + }, + loading: () => + const Center(child: LinearProgressIndicator()), + error: (error, stack) => const Text('Error')), + const SizedBox(height: 24.0), + AdaptiveDropdownMenu< + ProjectProjectIdUsersPostRequestRoleEnum>( + decoration: const InputDecoration( + labelText: 'Role', + contentPadding: EdgeInsets.all(8), + ), + value: formData.role, + onChanged: + (ProjectProjectIdUsersPostRequestRoleEnum? value) { + ref + .read(projectUserFormRequestProvider(user).notifier) + .updateWith(role: value); + }, + items: ProjectProjectIdUsersPostRequestRoleEnum.values.map< + AdaptiveDropdownMenuItem< + ProjectProjectIdUsersPostRequestRoleEnum>>( + (ProjectProjectIdUsersPostRequestRoleEnum value) { + return AdaptiveDropdownMenuItem< + ProjectProjectIdUsersPostRequestRoleEnum>( + value: value, + child: Text(value.name), + ); + }).toList(), + ), + const SizedBox(height: 24.0), + 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(projectUserFormRequestProvider(user) + .notifier) + .postUser(); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(userListProvider.notifier) + .loadRows(); + }, + child: const Text('Create')), + ]), + ), + ]), + ), + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => const Center(child: Text('Error')), + ); + } +} diff --git a/lib/components/user/role_form.dart b/lib/components/user/role_form.dart new file mode 100644 index 0000000..dd6b2d2 --- /dev/null +++ b/lib/components/user/role_form.dart @@ -0,0 +1,113 @@ +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:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dropdown.dart'; +import 'package:semaphore/state/projects/user.dart'; + +class ProjectUserRoleForm extends ConsumerWidget { + final int userId; + const ProjectUserRoleForm({super.key, required this.userId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); + final user = ref.watch(userFamily(userId)); + final formData = ref.watch(ProjectUserRoleFormRequestProvider(user.value)); + + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + + return user.when( + data: (user) { + 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('Update User Role', style: textTitleStyle), + ), + AdaptiveDropdownMenu< + ProjectProjectIdUsersUserIdPutRequestRoleEnum>( + decoration: const InputDecoration( + labelText: 'Role', + contentPadding: EdgeInsets.all(8), + ), + value: formData.role, + onChanged: (ProjectProjectIdUsersUserIdPutRequestRoleEnum? + value) { + ref + .read(projectUserRoleFormRequestProvider(user) + .notifier) + .updateWith(role: value); + }, + items: ProjectProjectIdUsersUserIdPutRequestRoleEnum + .values + .map< + AdaptiveDropdownMenuItem< + ProjectProjectIdUsersUserIdPutRequestRoleEnum>>( + (ProjectProjectIdUsersUserIdPutRequestRoleEnum + value) { + return AdaptiveDropdownMenuItem< + ProjectProjectIdUsersUserIdPutRequestRoleEnum>( + value: value, + child: Text(value.name), + ); + }).toList(), + ), + const SizedBox(height: 24.0), + 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(projectUserRoleFormRequestProvider( + user) + .notifier) + .postUser(userId); + if (context.mounted) { + Navigator.of(context).pop(); + } + ref + .read(userListProvider.notifier) + .loadRows(); + }, + child: const Text('Change')), + ]), + ), + ]), + ), + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => const Center(child: Text('Error')), + ); + } +} diff --git a/lib/components/user/system_form.dart b/lib/components/user/system_form.dart new file mode 100644 index 0000000..4c38e62 --- /dev/null +++ b/lib/components/user/system_form.dart @@ -0,0 +1,184 @@ +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/adaptive/button.dart'; +import 'package:semaphore/adaptive/switch.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/adaptive/text_field.dart'; +import 'package:semaphore/state/user.dart'; + +class SystemUserForm extends ConsumerWidget { + final int? userId; + const SystemUserForm({super.key, this.userId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final macosTheme = MacosTheme.of(context); + final systemUser = ref.watch(systemUserFamilyProvider(userId)); + final formData = ref.watch(systemUserFormRequestProvider(systemUser.value)); + + final textTitleStyle = Platform.isMacOS + ? macosTheme.typography.largeTitle + : theme.textTheme.titleLarge; + final textBodyStyle = Platform.isMacOS + ? macosTheme.typography.body + : theme.textTheme.bodyMedium; + + return systemUser.when( + data: (user) { + 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(user.id == null ? 'New User' : 'Edit User', + style: textTitleStyle), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user.name, + onChanged: (value) { + ref + .read(systemUserFormRequestProvider(user).notifier) + .updateWith(name: value); + }, + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user.username, + onChanged: (value) { + ref + .read(systemUserFormRequestProvider(user).notifier) + .updateWith(username: value); + }, + decoration: const InputDecoration( + labelText: 'Username', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user.email, + onChanged: (value) { + ref + .read(systemUserFormRequestProvider(user).notifier) + .updateWith(email: value); + }, + decoration: const InputDecoration( + labelText: 'Email', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + obscureText: true, + initialValue: formData.password, + onChanged: (value) { + if (value == '') { + ref + .read( + systemUserFormRequestProvider(user).notifier) + .unsetWith(password: true); + } else { + ref + .read( + systemUserFormRequestProvider(user).notifier) + .updateWith(password: value); + } + }, + decoration: const InputDecoration( + labelText: 'Password', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + Row( + children: [ + AdaptiveSwitch( + value: formData.alert ?? false, + onChanged: (value) { + ref + .read(systemUserFormRequestProvider(user) + .notifier) + .updateWith(alert: value); + }, + ), + const SizedBox(width: 12), + const AdaptiveTextBody('Send alert'), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + AdaptiveSwitch( + value: formData.admin ?? false, + onChanged: (value) { + ref + .read(systemUserFormRequestProvider(user) + .notifier) + .updateWith(admin: value); + }, + ), + const SizedBox(width: 12), + const AdaptiveTextBody('Admin user'), + ], + ), + const SizedBox(height: 24), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + AdaptiveButton( + onPressed: () async { + if (user.id == null) { + await ref + .read(systemUserFormRequestProvider(user) + .notifier) + .postUser(); + } else { + await ref + .read(systemUserFormRequestProvider(user) + .notifier) + .putUser(user.id!); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + await ref + .read(systemUserListProvider.notifier) + .loadRows(); + }, + child: const Text('Save'), + ), + AdaptiveTextButton( + onPressed: () async { + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: const Text('Cancel'), + ), + ], + ), + const SizedBox(height: 24), + ]), + ), + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => const Center(child: Text('Error')), + ); + } +} diff --git a/lib/database/database.dart b/lib/database/database.dart index eca84f3..1127a91 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -14,6 +14,7 @@ class Database { static Future initialize() async { final dir = await getApplicationSupportDirectory(); + print(dir.path); final isar = await Isar.open( name: 'GSMLG.app', allSchema, diff --git a/lib/database/schema.dart b/lib/database/schema.dart index d7949ad..c5a5d06 100644 --- a/lib/database/schema.dart +++ b/lib/database/schema.dart @@ -1,4 +1,8 @@ import 'package:isar/isar.dart' show CollectionSchema; -import 'schema/app_activities.dart'; +import 'package:semaphore/database/schema/semaphore_server.dart'; +import 'package:semaphore/database/schema/app_activities.dart'; -const List allSchema = [AppActivitiesSchema]; +const List allSchema = [ + AppActivitiesSchema, + SemaphoreServerSchema +]; diff --git a/lib/database/schema/app_activities.dart b/lib/database/schema/app_activities.dart index 101d18c..db22e9f 100644 --- a/lib/database/schema/app_activities.dart +++ b/lib/database/schema/app_activities.dart @@ -1,20 +1,14 @@ +import 'package:flutter/scheduler.dart' show AppLifecycleState; import 'package:isar/isar.dart'; part 'app_activities.g.dart'; -enum ActiveState { - active, - deactive, - pause, - resume, -} - @collection class AppActivities { Id id = Isar.autoIncrement; @enumerated - late ActiveState state; + late AppLifecycleState state; DateTime createdAt = DateTime.now(); } diff --git a/lib/database/schema/app_activities.g.dart b/lib/database/schema/app_activities.g.dart index d288204..d1ac5d3 100644 --- a/lib/database/schema/app_activities.g.dart +++ b/lib/database/schema/app_activities.g.dart @@ -73,7 +73,7 @@ AppActivities _appActivitiesDeserialize( object.id = id; object.state = _AppActivitiesstateValueEnumMap[reader.readByteOrNull(offsets[1])] ?? - ActiveState.active; + AppLifecycleState.detached; return object; } @@ -88,23 +88,25 @@ P _appActivitiesDeserializeProp

( return (reader.readDateTime(offset)) as P; case 1: return (_AppActivitiesstateValueEnumMap[reader.readByteOrNull(offset)] ?? - ActiveState.active) as P; + AppLifecycleState.detached) as P; default: throw IsarError('Unknown property with id $propertyId'); } } const _AppActivitiesstateEnumValueMap = { - 'active': 0, - 'deactive': 1, - 'pause': 2, - 'resume': 3, + 'detached': 0, + 'resumed': 1, + 'inactive': 2, + 'hidden': 3, + 'paused': 4, }; const _AppActivitiesstateValueEnumMap = { - 0: ActiveState.active, - 1: ActiveState.deactive, - 2: ActiveState.pause, - 3: ActiveState.resume, + 0: AppLifecycleState.detached, + 1: AppLifecycleState.resumed, + 2: AppLifecycleState.inactive, + 3: AppLifecycleState.hidden, + 4: AppLifecycleState.paused, }; Id _appActivitiesGetId(AppActivities object) { @@ -314,7 +316,7 @@ extension AppActivitiesQueryFilter } QueryBuilder - stateEqualTo(ActiveState value) { + stateEqualTo(AppLifecycleState value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'state', @@ -325,7 +327,7 @@ extension AppActivitiesQueryFilter QueryBuilder stateGreaterThan( - ActiveState value, { + AppLifecycleState value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -339,7 +341,7 @@ extension AppActivitiesQueryFilter QueryBuilder stateLessThan( - ActiveState value, { + AppLifecycleState value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -353,8 +355,8 @@ extension AppActivitiesQueryFilter QueryBuilder stateBetween( - ActiveState lower, - ActiveState upper, { + AppLifecycleState lower, + AppLifecycleState upper, { bool includeLower = true, bool includeUpper = true, }) { @@ -473,7 +475,8 @@ extension AppActivitiesQueryProperty }); } - QueryBuilder stateProperty() { + QueryBuilder + stateProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'state'); }); diff --git a/lib/database/schema/semaphore_server.dart b/lib/database/schema/semaphore_server.dart new file mode 100644 index 0000000..0fdade1 --- /dev/null +++ b/lib/database/schema/semaphore_server.dart @@ -0,0 +1,22 @@ +import 'package:isar/isar.dart'; + +part 'semaphore_server.g.dart'; + +@collection +class SemaphoreServer { + Id id = Isar.autoIncrement; + + String? name; + + String? apiUrl; + + bool? isActive; + + String? username; + + String? password; + + String? token; + + DateTime createdAt = DateTime.now(); +} diff --git a/lib/database/schema/semaphore_server.g.dart b/lib/database/schema/semaphore_server.g.dart new file mode 100644 index 0000000..be92e70 --- /dev/null +++ b/lib/database/schema/semaphore_server.g.dart @@ -0,0 +1,1490 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'semaphore_server.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetSemaphoreServerCollection on Isar { + IsarCollection get semaphoreServers => this.collection(); +} + +const SemaphoreServerSchema = CollectionSchema( + name: r'SemaphoreServer', + id: -5077152365400546886, + properties: { + r'apiUrl': PropertySchema( + id: 0, + name: r'apiUrl', + type: IsarType.string, + ), + r'createdAt': PropertySchema( + id: 1, + name: r'createdAt', + type: IsarType.dateTime, + ), + r'isActive': PropertySchema( + id: 2, + name: r'isActive', + type: IsarType.bool, + ), + r'name': PropertySchema( + id: 3, + name: r'name', + type: IsarType.string, + ), + r'password': PropertySchema( + id: 4, + name: r'password', + type: IsarType.string, + ), + r'token': PropertySchema( + id: 5, + name: r'token', + type: IsarType.string, + ), + r'username': PropertySchema( + id: 6, + name: r'username', + type: IsarType.string, + ) + }, + estimateSize: _semaphoreServerEstimateSize, + serialize: _semaphoreServerSerialize, + deserialize: _semaphoreServerDeserialize, + deserializeProp: _semaphoreServerDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _semaphoreServerGetId, + getLinks: _semaphoreServerGetLinks, + attach: _semaphoreServerAttach, + version: '3.1.0+1', +); + +int _semaphoreServerEstimateSize( + SemaphoreServer object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.apiUrl; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.name; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.password; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.token; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.username; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _semaphoreServerSerialize( + SemaphoreServer object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.apiUrl); + writer.writeDateTime(offsets[1], object.createdAt); + writer.writeBool(offsets[2], object.isActive); + writer.writeString(offsets[3], object.name); + writer.writeString(offsets[4], object.password); + writer.writeString(offsets[5], object.token); + writer.writeString(offsets[6], object.username); +} + +SemaphoreServer _semaphoreServerDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SemaphoreServer(); + object.apiUrl = reader.readStringOrNull(offsets[0]); + object.createdAt = reader.readDateTime(offsets[1]); + object.id = id; + object.isActive = reader.readBoolOrNull(offsets[2]); + object.name = reader.readStringOrNull(offsets[3]); + object.password = reader.readStringOrNull(offsets[4]); + object.token = reader.readStringOrNull(offsets[5]); + object.username = reader.readStringOrNull(offsets[6]); + return object; +} + +P _semaphoreServerDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readDateTime(offset)) as P; + case 2: + return (reader.readBoolOrNull(offset)) as P; + case 3: + return (reader.readStringOrNull(offset)) as P; + case 4: + return (reader.readStringOrNull(offset)) as P; + case 5: + return (reader.readStringOrNull(offset)) as P; + case 6: + return (reader.readStringOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _semaphoreServerGetId(SemaphoreServer object) { + return object.id; +} + +List> _semaphoreServerGetLinks(SemaphoreServer object) { + return []; +} + +void _semaphoreServerAttach( + IsarCollection col, Id id, SemaphoreServer object) { + object.id = id; +} + +extension SemaphoreServerQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SemaphoreServerQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension SemaphoreServerQueryFilter + on QueryBuilder { + QueryBuilder + apiUrlIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'apiUrl', + )); + }); + } + + QueryBuilder + apiUrlIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'apiUrl', + )); + }); + } + + QueryBuilder + apiUrlEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'apiUrl', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'apiUrl', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'apiUrl', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + apiUrlIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'apiUrl', + value: '', + )); + }); + } + + QueryBuilder + apiUrlIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'apiUrl', + value: '', + )); + }); + } + + QueryBuilder + createdAtEqualTo(DateTime value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtGreaterThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtLessThan( + DateTime value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtBetween( + DateTime lower, + DateTime upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'createdAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + isActiveIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'isActive', + )); + }); + } + + QueryBuilder + isActiveIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'isActive', + )); + }); + } + + QueryBuilder + isActiveEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isActive', + value: value, + )); + }); + } + + QueryBuilder + nameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'name', + )); + }); + } + + QueryBuilder + nameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'name', + )); + }); + } + + QueryBuilder + nameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'name', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder + passwordIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'password', + )); + }); + } + + QueryBuilder + passwordIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'password', + )); + }); + } + + QueryBuilder + passwordEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'password', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'password', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'password', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + passwordIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'password', + value: '', + )); + }); + } + + QueryBuilder + passwordIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'password', + value: '', + )); + }); + } + + QueryBuilder + tokenIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'token', + )); + }); + } + + QueryBuilder + tokenIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'token', + )); + }); + } + + QueryBuilder + tokenEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'token', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'token', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'token', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'token', + value: '', + )); + }); + } + + QueryBuilder + tokenIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'token', + value: '', + )); + }); + } + + QueryBuilder + usernameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'username', + )); + }); + } + + QueryBuilder + usernameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'username', + )); + }); + } + + QueryBuilder + usernameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'username', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'username', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'username', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + usernameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'username', + value: '', + )); + }); + } + + QueryBuilder + usernameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'username', + value: '', + )); + }); + } +} + +extension SemaphoreServerQueryObject + on QueryBuilder {} + +extension SemaphoreServerQueryLinks + on QueryBuilder {} + +extension SemaphoreServerQuerySortBy + on QueryBuilder { + QueryBuilder sortByApiUrl() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'apiUrl', Sort.asc); + }); + } + + QueryBuilder + sortByApiUrlDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'apiUrl', Sort.desc); + }); + } + + QueryBuilder + sortByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + sortByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder + sortByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.asc); + }); + } + + QueryBuilder + sortByIsActiveDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.desc); + }); + } + + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder + sortByPassword() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'password', Sort.asc); + }); + } + + QueryBuilder + sortByPasswordDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'password', Sort.desc); + }); + } + + QueryBuilder sortByToken() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'token', Sort.asc); + }); + } + + QueryBuilder + sortByTokenDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'token', Sort.desc); + }); + } + + QueryBuilder + sortByUsername() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'username', Sort.asc); + }); + } + + QueryBuilder + sortByUsernameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'username', Sort.desc); + }); + } +} + +extension SemaphoreServerQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByApiUrl() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'apiUrl', Sort.asc); + }); + } + + QueryBuilder + thenByApiUrlDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'apiUrl', Sort.desc); + }); + } + + QueryBuilder + thenByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder + thenByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.asc); + }); + } + + QueryBuilder + thenByIsActiveDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isActive', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder + thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder + thenByPassword() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'password', Sort.asc); + }); + } + + QueryBuilder + thenByPasswordDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'password', Sort.desc); + }); + } + + QueryBuilder thenByToken() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'token', Sort.asc); + }); + } + + QueryBuilder + thenByTokenDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'token', Sort.desc); + }); + } + + QueryBuilder + thenByUsername() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'username', Sort.asc); + }); + } + + QueryBuilder + thenByUsernameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'username', Sort.desc); + }); + } +} + +extension SemaphoreServerQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByApiUrl( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'apiUrl', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'createdAt'); + }); + } + + QueryBuilder + distinctByIsActive() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isActive'); + }); + } + + QueryBuilder distinctByName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByPassword( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'password', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByToken( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'token', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByUsername( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'username', caseSensitive: caseSensitive); + }); + } +} + +extension SemaphoreServerQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder apiUrlProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'apiUrl'); + }); + } + + QueryBuilder + createdAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'createdAt'); + }); + } + + QueryBuilder isActiveProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isActive'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } + + QueryBuilder passwordProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'password'); + }); + } + + QueryBuilder tokenProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'token'); + }); + } + + QueryBuilder usernameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'username'); + }); + } +} diff --git a/lib/main.dart b/lib/main.dart index de4e7cf..3400a8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,10 +7,11 @@ 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'; +import 'package:semaphore/database/database.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + SystemTheme.fallbackColor = const Color(0xFF005057); await SystemTheme.accentColor.load(); if (!kIsWeb) { @@ -20,8 +21,8 @@ void main() async { } } - // await Database.initialize(); - // await Database.performMigrationIfNeeded(); + await Database.initialize(); + await Database.performMigrationIfNeeded(); runApp( const ProviderScope(observers: [StateLogger()], child: App()), diff --git a/lib/router/router_notifier.dart b/lib/router/router_notifier.dart index 486f7a1..38409f1 100644 --- a/lib/router/router_notifier.dart +++ b/lib/router/router_notifier.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/screens/profile/profile_screen.dart'; import 'package:semaphore/screens/project/environment_screen.dart'; -import 'package:semaphore/screens/project/history_screen.dart'; +import 'package:semaphore/screens/project/dashboard_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/screens/setting/setting_screen.dart'; import 'package:semaphore/screens/splash/splash_screen.dart'; -import 'package:semaphore/screens/sign_in/sign_in_screen.dart'; import 'package:semaphore/screens/home/home_screen.dart'; -import 'package:semaphore/screens/url_config/url_config_screen.dart'; part 'router_notifier.g.dart'; @@ -36,7 +36,7 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { final isSplash = state.path == SplashScreen.path; if (isSplash) { - return isAuth ? HomeScreen.path : SignInScreen.path; + // return isAuth ? HomeScreen.path : SettingsScreen.path; } return null; } @@ -53,25 +53,36 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { }, ), GoRoute( - name: UrlConfigScreen.name, - path: UrlConfigScreen.path, + name: SettingsScreen.name, + path: SettingsScreen.path, pageBuilder: (context, state) { + final name = state.pathParameters['module']; + final module = SettingsScreenModule.values.firstWhere( + (m) => m.name == name, + orElse: () => SettingsScreenModule.values.first); + return MaterialPage( key: state.pageKey, - child: const UrlConfigScreen(), + child: SettingsScreen(module: module), ); }, + redirect: (context, state) => isAuth ? SettingsScreen.path : null, ), GoRoute( - name: SignInScreen.name, - path: SignInScreen.path, + name: ProfileScreen.name, + path: ProfileScreen.path, pageBuilder: (context, state) { + final name = state.pathParameters['module']; + final module = ProfileScreenModule.values.firstWhere( + (m) => m.name == name, + orElse: () => ProfileScreenModule.values.first); + return MaterialPage( key: state.pageKey, - child: const SignInScreen(), + child: ProfileScreen(module: module), ); }, - redirect: (context, state) => isAuth ? HomeScreen.path : null, + redirect: (context, state) => isAuth ? ProfileScreen.path : null, ), GoRoute( name: HomeScreen.name, @@ -84,12 +95,12 @@ class RouterNotifier extends _$RouterNotifier implements Listenable { }, routes: const []), GoRoute( - name: HistoryScreen.name, - path: HistoryScreen.path, + name: DashboardScreen.name, + path: DashboardScreen.path, pageBuilder: (context, state) { return NoTransitionPage( key: state.pageKey, - child: const HistoryScreen(), + child: const DashboardScreen(), ); }, routes: const []), diff --git a/lib/router/router_notifier.g.dart b/lib/router/router_notifier.g.dart index b190d52..1ef2288 100644 --- a/lib/router/router_notifier.g.dart +++ b/lib/router/router_notifier.g.dart @@ -6,7 +6,7 @@ part of 'router_notifier.dart'; // RiverpodGenerator // ************************************************************************** -String _$routerNotifierHash() => r'c0cbb262d3ac89c5f248485dce1aac2e50597e17'; +String _$routerNotifierHash() => r'b8bbb45e4d215f74101bd210c95787f5c90348f1'; /// See also [RouterNotifier]. @ProviderFor(RouterNotifier) @@ -23,4 +23,4 @@ final routerNotifierProvider = typedef _$RouterNotifier = AutoDisposeAsyncNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 9e04dc8..d35b1ae 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -1,12 +1,14 @@ import 'dart:async'; +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/adaptive/scaffold.dart'; import 'package:semaphore/components/app_bar.dart'; import 'package:semaphore/components/app_drawer.dart'; import 'package:semaphore/router/router.dart'; -import 'package:semaphore/screens/project/history_screen.dart'; +import 'package:semaphore/screens/project/dashboard_screen.dart'; import 'package:semaphore/state/projects.dart'; class HomeScreen extends ConsumerWidget { @@ -19,8 +21,13 @@ class HomeScreen extends ConsumerWidget { final size = MediaQuery.of(context).size; final width = size.width > size.height ? size.height : size.width; final textSize = width * 0.618 * 0.1; + Color? textColor = Theme.of(context).colorScheme.onSurface; + if (Platform.isMacOS) { + textColor = MacosTheme.of(context).typography.largeTitle.color; + } + final textStyle = TextStyle(fontSize: textSize, color: textColor); - ref.read(projectsProvider.notifier).getProjects(); + ref.read(projectsProvider.notifier).reloadProjects(); final currentProject = ref.watch(currentProjectProvider); return AdaptiveScaffold( @@ -35,7 +42,7 @@ class HomeScreen extends ConsumerWidget { child: currentProject.when( data: (current) { Timer(const Duration(milliseconds: 618), () { - ref.read(routerProvider).goNamed(HistoryScreen.name, + ref.read(routerProvider).goNamed(DashboardScreen.name, pathParameters: { 'pid': '${current?.id ?? 1}', }); @@ -44,9 +51,9 @@ class HomeScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Ansible', style: TextStyle(fontSize: textSize)), + Text('Ansible', style: textStyle), SizedBox(height: textSize), - Text('Semaphore', style: TextStyle(fontSize: textSize)), + Text('Semaphore', style: textStyle), ], )); }, diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart new file mode 100644 index 0000000..4330631 --- /dev/null +++ b/lib/screens/profile/profile_screen.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; +import 'package:semaphore/adaptive/tab_view.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/components/app_bar.dart'; +import 'package:semaphore/components/user/system_form.dart'; +import 'package:semaphore/router/router.dart'; +import 'package:semaphore/screens/home/home_screen.dart'; +import 'package:semaphore/screens/profile/user_app_activity.dart'; +import 'package:semaphore/screens/profile/user_info.dart'; +import 'package:semaphore/screens/profile/system_user_list.dart'; +import 'package:semaphore/state/auth.dart'; + +enum ProfileScreenModule { info, list, activity } + +extension ProfileScreenModuleWidget on ProfileScreenModule { + bool get adminOnly { + switch (this) { + case ProfileScreenModule.list: + return true; + default: + return false; + } + } + + Widget get wiget { + switch (this) { + case ProfileScreenModule.info: + return const UserInfo(); + case ProfileScreenModule.list: + return const SystemUserList(); + case ProfileScreenModule.activity: + return const UserAppActivity(); + } + } + + List actions(BuildContext context) { + switch (this) { + case ProfileScreenModule.info: + return []; + case ProfileScreenModule.list: + return [ + AdaptiveIconButton( + onPressed: () { + adaptiveDialog( + context: context, child: const SystemUserForm(userId: null)); + }, + icon: const AdaptiveIcon(Icons.add), + ) + ]; + case ProfileScreenModule.activity: + return []; + } + } +} + +class ProfileScreen extends ConsumerWidget { + final ProfileScreenModule module; + + const ProfileScreen({super.key, required this.module}); + + static const name = 'profile'; + static const path = '/profile/:module'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(currentUserProvider); + + return AdaptiveScaffold( + appBar: LocalAppBar( + title: 'Profile', + // actions: module.actions(context), + leading: AdaptiveIconButton( + icon: const AdaptiveIcon(Icons.arrow_back_ios), + onPressed: () { + ref.read(routerProvider).goNamed(HomeScreen.name); + }, + )), + body: SafeArea( + child: user.when( + data: (user) => AdaptiveTabView( + initialIndex: ProfileScreenModule.values.indexOf(module), + tabs: ProfileScreenModule.values + .where((element) => + element.adminOnly ? user?.admin == true : true) + .map((m) => m.name) + .toList(), + children: ProfileScreenModule.values + .where((element) => + element.adminOnly ? user?.admin == true : true) + .map((m) => m.wiget) + .toList(), + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( + child: AdaptiveTextError(error.toString()), + ), + ), + )); + } +} diff --git a/lib/screens/profile/system_user_list.dart b/lib/screens/profile/system_user_list.dart new file mode 100644 index 0000000..ca5408c --- /dev/null +++ b/lib/screens/profile/system_user_list.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/components/user/system_form.dart'; +import 'package:semaphore/state/user.dart'; + +class SystemUserList extends ConsumerWidget { + const SystemUserList({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final systemUserList = ref.watch(systemUserListProvider); + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: PlutoGrid( + mode: PlutoGridMode.readOnly, + createHeader: (PlutoGridStateManager stateManager) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveIconButton( + onPressed: () { + adaptiveDialog( + context: context, + child: const SystemUserForm(userId: null)); + }, + icon: const AdaptiveIcon(Icons.add), + ), + ], + ); + }, + columns: systemUserList.columns, + rows: systemUserList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(systemUserListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: systemUserList.configurationWithTheme(context), + ))); + } +} diff --git a/lib/screens/profile/user_app_activity.dart b/lib/screens/profile/user_app_activity.dart new file mode 100644 index 0000000..0fad290 --- /dev/null +++ b/lib/screens/profile/user_app_activity.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/state/user_app_activity.dart'; + +class UserAppActivity extends ConsumerWidget { + const UserAppActivity({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(userAppActivityDataProvider); + + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: SingleChildScrollView( + child: data.when( + data: (data) => Column( + children: [ + const AdaptiveTextTitle('User App Activity'), + DataTable( + columns: const [ + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'ID', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'State', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Created At', + ), + ), + ), + ], + rows: data.map((e) { + return DataRow( + cells: [ + DataCell(AdaptiveTextBody(e.id.toString())), + DataCell(AdaptiveTextBody(e.state.name)), + DataCell(AdaptiveTextBody(e.createdAt.toString())), + ], + ); + }).toList(), + ) + ], + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( + child: Text(error.toString()), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/profile/user_info.dart b/lib/screens/profile/user_info.dart new file mode 100644 index 0000000..5dcb429 --- /dev/null +++ b/lib/screens/profile/user_info.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/switch.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/adaptive/text_field.dart'; +import 'package:semaphore/state/auth.dart'; + +class UserInfo extends ConsumerWidget { + const UserInfo({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(currentUserProvider); + + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: SingleChildScrollView( + child: user.when( + data: (user) => Column( + children: [ + const AdaptiveTextTitle('User Info'), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user?.name, + onChanged: (value) {}, + decoration: const InputDecoration( + labelText: 'Name', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user?.username, + onChanged: (value) {}, + decoration: const InputDecoration( + labelText: 'Username', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + AdaptiveTextField( + autocorrect: false, + initialValue: user?.email, + onChanged: (value) {}, + decoration: const InputDecoration( + labelText: 'Email', contentPadding: EdgeInsets.all(8)), + ), + const SizedBox(height: 24), + Row( + children: [ + AdaptiveSwitch( + value: user?.alert ?? false, + onChanged: (value) {}, + ), + const SizedBox(width: 12), + const AdaptiveTextBody('Send alert'), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + AdaptiveSwitch( + value: user?.admin ?? false, + ), + const SizedBox(width: 12), + const AdaptiveTextBody('Admin user'), + ], + ), + const SizedBox(height: 24), + AdaptiveButton( + onPressed: () {}, + child: const Text('Save'), + ), + const SizedBox(height: 24), + ], + ), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center(child: Text(error.toString())), + ), + ), + ), + ); + } +} diff --git a/lib/screens/project/dashboard_screen.dart b/lib/screens/project/dashboard_screen.dart new file mode 100644 index 0000000..9df0713 --- /dev/null +++ b/lib/screens/project/dashboard_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pluto_grid/pluto_grid.dart'; +import 'package:semaphore/adaptive/alert_dialog.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/checkbox.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; +import 'package:semaphore/adaptive/tab_view.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/adaptive/text_field.dart'; +import 'package:semaphore/components/app_bar.dart'; +import 'package:semaphore/components/app_drawer.dart'; +import 'package:semaphore/state/projects.dart'; +import 'package:semaphore/state/projects/task.dart'; +import 'package:semaphore/state/projects/activity.dart'; + +class DashboardScreen extends ConsumerWidget { + const DashboardScreen({super.key}); + static const name = 'projectDashboard'; + static const path = '/projects/:pid/dashboard'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AdaptiveScaffold( + drawer: const LocalDrawer(), + appBar: const LocalAppBar(title: 'Dashboard'), + body: SafeArea( + child: AdaptiveTabView( + tabs: const ['Hisotry', 'Activity', 'Settings'], + children: [ + Consumer(builder: (context, ref, _) { + final taskList = ref.watch(taskListProvider); + + return 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), + ); + }), + Consumer(builder: (context, ref, _) { + final activityList = ref.watch(activityListProvider); + + return PlutoGrid( + mode: PlutoGridMode.readOnly, + columns: activityList.columns, + rows: activityList.rows, + noRowsWidget: null, + onLoaded: (PlutoGridOnLoadedEvent event) { + ref + .read(activityListProvider.notifier) + .setStateManager(event.stateManager); + }, + onChanged: (PlutoGridOnChangedEvent event) {}, + configuration: activityList.configurationWithTheme(context), + ); + }), + Consumer(builder: (context, ref, _) { + final currentProject = ref.watch(currentProjectProvider); + + return Padding( + padding: const EdgeInsets.all(24.0), + child: SingleChildScrollView( + child: Column( + children: [ + Form( + child: currentProject.when( + data: (project) { + final formData = + ref.watch(projectFormStateProvider(project)); + + return Column( + children: [ + AdaptiveTextField( + autocorrect: false, + initialValue: formData.name, + onChanged: (value) { + ref + .read( + projectFormStateProvider(project) + .notifier) + .updateWith(name: value); + }, + decoration: const InputDecoration( + labelText: 'Project Name', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox( + height: 24.0, + ), + Row( + children: [ + AdaptiveCheckbox( + value: formData.alert, + onChanged: (bool value) { + ref + .read(projectFormStateProvider( + project) + .notifier) + .updateWith(alert: value); + }, + ), + const SizedBox( + width: 12.0, + ), + const AdaptiveTextBody( + 'Allow alerts for this project'), + ], + ), + const SizedBox( + height: 24.0, + ), + AdaptiveTextField( + autocorrect: false, + initialValue: formData.alertChat, + onChanged: (value) { + ref + .read( + projectFormStateProvider(project) + .notifier) + .updateWith(alertChat: value); + }, + decoration: const InputDecoration( + labelText: + 'Telegram Chat ID (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox( + height: 24.0, + ), + AdaptiveTextField( + autocorrect: false, + initialValue: + formData.maxParallelTasks.toString(), + onChanged: (value) { + ref + .read( + projectFormStateProvider(project) + .notifier) + .updateWith( + maxParallelTasks: + int.tryParse(value) ?? 0); + }, + decoration: const InputDecoration( + labelText: + 'Max number of parallel tasks (Optional)', + contentPadding: EdgeInsets.all(8)), + ), + const SizedBox( + height: 24.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AdaptiveButton( + onPressed: () { + ref + .read(projectsProvider.notifier) + .updateProject(formData); + }, + child: const Text('Save'), + ), + ], + ), + const SizedBox( + height: 36.0, + ), + const Divider(), + const SizedBox( + height: 36.0, + ), + Wrap( + spacing: 12.0, + runSpacing: 12.0, + children: [ + AdaptiveButton( + color: Colors.redAccent, + onPressed: () { + adaptiveAlertDialog( + context: context, + title: const AdaptiveTextTitle( + 'DELETE PROJECT', + ), + content: const Column( + children: [ + AdaptiveTextBody( + 'Are you sure you want to delete this project?', + ), + SizedBox( + height: 16.0, + ), + AdaptiveTextError( + 'Once you delete a project, there is no going back. Please be certain.', + ), + SizedBox( + height: 16.0, + ), + ], + ), + primaryButton: AdaptiveButton( + color: Colors.redAccent, + onPressed: () async { + Navigator.of(context).pop(); + await ref + .read(projectsProvider + .notifier) + .deleteProject(project!); + }, + child: const Text('DELETE'), + ), + secondaryButton: AdaptiveButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('CANCEL'), + ), + ); + }, + child: const Text('DELETE PROJECT'), + ), + const AdaptiveTextError( + 'Once you delete a project, there is no going back. Please be certain.', + ), + ], + ), + ], + ); + }, + loading: () => const LinearProgressIndicator(), + error: (error, stackTrace) => SizedBox( + child: Text(error.toString()), + ), + ), + ), + ], + ), + ), + ); + }), + ], + ), + )); + } +} diff --git a/lib/screens/project/team_screen.dart b/lib/screens/project/team_screen.dart index 1519fa7..ef4f837 100644 --- a/lib/screens/project/team_screen.dart +++ b/lib/screens/project/team_screen.dart @@ -2,10 +2,13 @@ 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/user/form.dart'; import 'package:semaphore/state/projects/user.dart'; class TeamScreen extends ConsumerWidget { @@ -24,8 +27,10 @@ class TeamScreen extends ConsumerWidget { drawer: const LocalDrawer(), appBar: const LocalAppBar(title: 'Team'), floatingAction: AdaptiveFloatingAction( - icon: const Icon(Icons.add), - onPressed: () {}, + icon: const AdaptiveIcon(Icons.add), + onPressed: () { + adaptiveDialog(context: context, child: const ProjectUserForm()); + }, ), body: SafeArea( child: PlutoGrid( diff --git a/lib/screens/setting/setting_project.dart b/lib/screens/setting/setting_project.dart new file mode 100644 index 0000000..7c0a4af --- /dev/null +++ b/lib/screens/setting/setting_project.dart @@ -0,0 +1,85 @@ +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/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/components/project/form.dart'; +import 'package:semaphore/state/projects.dart'; + +class ProjectSetting extends ConsumerWidget { + const ProjectSetting({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + Color? primaryColor = Theme.of(context).colorScheme.primary; + if (Platform.isMacOS) { + primaryColor = MacosTheme.of(context).primaryColor; + } + + final projects = ref.watch(projectsProvider); + final currentProject = ref.watch(currentProjectProvider); + + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: ListView( + children: [ + Wrap( + direction: Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16.0, + runSpacing: 16.0, + children: [ + const AdaptiveTextTitle('Switch Projects'), + AdaptiveButton( + color: primaryColor, + child: const Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.add, color: Colors.white), + Text('Add', style: TextStyle(color: Colors.white)), + ]), + onPressed: () { + adaptiveDialog(context: context, child: const ProjectForm()); + }, + ), + ], + ), + ...projects.when( + data: (projects) => projects + .map((project) => ListTile( + leading: AdaptiveTextBody(project.id.toString()), + title: AdaptiveTextBody(project.name ?? '--'), + subtitle: AdaptiveTextBody(project.created ?? '--'), + trailing: currentProject.when( + data: (p) => p?.id == project.id + ? const IconButton( + onPressed: null, + icon: + AdaptiveIcon(Icons.radio_button_checked)) + : IconButton( + onPressed: () { + ref + .read(currentProjectProvider.notifier) + .setCurrent(project); + }, + icon: const AdaptiveIcon( + Icons.radio_button_unchecked)), + error: (error, stack) => + AdaptiveTextBody('Error: ${error.toString()}'), + loading: () => const CircularProgressIndicator()), + )) + .toList(), + loading: () => [const Center(child: CircularProgressIndicator())], + error: (error, stackTrace) => [ + const Text('Error'), + Text(error.toString()), + ], + ), + ], + ), + )); + } +} diff --git a/lib/screens/setting/setting_screen.dart b/lib/screens/setting/setting_screen.dart new file mode 100644 index 0000000..9b75113 --- /dev/null +++ b/lib/screens/setting/setting_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/adaptive/scaffold.dart'; +import 'package:semaphore/adaptive/tab_view.dart'; +import 'package:semaphore/components/app_bar.dart'; +import 'package:semaphore/router/router.dart'; +import 'package:semaphore/screens/home/home_screen.dart'; +import 'package:semaphore/screens/setting/setting_project.dart'; +import 'package:semaphore/screens/setting/setting_server.dart'; +import 'package:semaphore/screens/setting/setting_theme.dart'; +import 'package:semaphore/state/server.dart'; + +enum SettingsScreenModule { theme, server, project } + +extension SettingsScreenModuleWidget on SettingsScreenModule { + Widget get wiget { + switch (this) { + case SettingsScreenModule.theme: + return const ThemeSetting(); + case SettingsScreenModule.server: + return const ServerSetting(); + case SettingsScreenModule.project: + return const ProjectSetting(); + } + } +} + +class SettingsScreen extends ConsumerWidget { + final SettingsScreenModule module; + + const SettingsScreen({super.key, required this.module}); + + static const name = 'settings'; + static const path = '/settings/:module'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentServer = ref.watch(serversProvider.notifier).currentServer(); + + return AdaptiveScaffold( + appBar: LocalAppBar( + title: 'Settings', + leading: currentServer == null + ? const SizedBox() + : AdaptiveIconButton( + icon: const AdaptiveIcon(Icons.arrow_back_ios), + onPressed: () { + ref.read(routerProvider).goNamed(HomeScreen.name); + }, + )), + body: SafeArea( + child: AdaptiveTabView( + initialIndex: SettingsScreenModule.values.indexOf(module), + tabs: + SettingsScreenModule.values.map((m) => m.name).toList(), + children: SettingsScreenModule.values + .map((m) => m.wiget) + .toList(), + ), + )); + } +} diff --git a/lib/screens/setting/setting_server.dart b/lib/screens/setting/setting_server.dart new file mode 100644 index 0000000..d47948a --- /dev/null +++ b/lib/screens/setting/setting_server.dart @@ -0,0 +1,175 @@ +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/adaptive/alert_dialog.dart'; +import 'package:semaphore/adaptive/button.dart'; +import 'package:semaphore/adaptive/dialog.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/icon_button.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/adaptive/text_timeago.dart'; +import 'package:semaphore/components/server/form.dart'; +import 'package:semaphore/state/auth.dart'; +import 'package:semaphore/state/server.dart'; + +class ServerSetting extends ConsumerWidget { + const ServerSetting({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final servers = ref.watch(serversProvider); + Color? primaryColor = Theme.of(context).colorScheme.primary; + if (Platform.isMacOS) { + primaryColor = MacosTheme.of(context).primaryColor; + } + + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: SingleChildScrollView( + child: Column( + children: [ + Wrap( + direction: Axis.horizontal, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16.0, + runSpacing: 16.0, + children: [ + const AdaptiveTextTitle('Manage Semaphore Servers'), + AdaptiveButton( + color: primaryColor, + child: + const Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.add, color: Colors.white), + Text('Add', style: TextStyle(color: Colors.white)), + ]), + onPressed: () { + adaptiveDialog( + context: context, + child: const ServerForm(serverId: null)); + }, + ), + ], + ), + DataTable( + columns: const [ + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Active', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'ID', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Name', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'API URL', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Username', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Created At', + ), + ), + ), + DataColumn( + label: Expanded( + child: AdaptiveTextTitle( + 'Actions', + ), + ), + ), + ], + rows: servers.map((s) { + return DataRow( + cells: [ + DataCell( + s.isActive == true + ? const AdaptiveIcon(Icons.check_box_outlined) + : AdaptiveIconButton( + onPressed: () { + ref + .read(serversProvider.notifier) + .activeServer(s); + }, + icon: const AdaptiveIcon( + Icons.check_box_outline_blank)), + ), + DataCell(AdaptiveTextBody(s.id.toString())), + DataCell(AdaptiveTextBody(s.name!)), + DataCell(AdaptiveTextBody(s.apiUrl!)), + DataCell(AdaptiveTextBody(s.username!)), + DataCell(AdaptiveTextTimeago(s.createdAt)), + DataCell(Wrap( + children: [ + AdaptiveIconButton( + onPressed: () { + adaptiveDialog( + context: context, + child: ServerForm(serverId: s.id)); + }, + icon: const AdaptiveIcon(Icons.edit), + ), + AdaptiveIconButton( + onPressed: () { + adaptiveAlertDialog( + context: context, + title: + const AdaptiveTextTitle('Delete Server'), + content: const AdaptiveTextBody( + 'Are you sure you want to delete this server?'), + primaryButton: AdaptiveButton( + color: Colors.redAccent, + onPressed: () async { + ref + .read(serversProvider.notifier) + .removeServer(s); + Navigator.of(context).pop(); + }, + child: const Text('Delete')), + secondaryButton: AdaptiveButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel')), + ); + }, + icon: const AdaptiveIcon(Icons.delete), + ), + ], + )), + ], + ); + }).toList(), + ) + ], + ), + )), + ); + } +} diff --git a/lib/screens/setting/setting_theme.dart b/lib/screens/setting/setting_theme.dart new file mode 100644 index 0000000..e6c0884 --- /dev/null +++ b/lib/screens/setting/setting_theme.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:semaphore/adaptive/icon.dart'; +import 'package:semaphore/adaptive/text.dart'; +import 'package:semaphore/state/theme.dart'; + +class ThemeSetting extends ConsumerWidget { + const ThemeSetting({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(themeModeProvider); + + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Wrap( + spacing: 24.0, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const AdaptiveTextTitle('Theme Mode'), + ToggleButtons( + isSelected: + ThemeMode.values.map((tm) => tm == themeMode).toList(), + onPressed: (idx) { + final tm = ThemeMode.values[idx]; + ref.read(themeModeProvider.notifier).changeThemeMode(tm); + }, + children: ThemeMode.values + .map((tm) => Wrap( + spacing: 12, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const SizedBox( + width: 12.0, + height: 12.0, + ), + AdaptiveIcon(tm.iconData), + AdaptiveTextBody(tm.title), + const SizedBox( + width: 12.0, + height: 12.0, + ), + ], + )) + .toList()), + ], + ), + )); + } +} diff --git a/lib/screens/splash/splash_screen.dart b/lib/screens/splash/splash_screen.dart index 783ba31..219776e 100644 --- a/lib/screens/splash/splash_screen.dart +++ b/lib/screens/splash/splash_screen.dart @@ -1,13 +1,11 @@ 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'; -import 'package:semaphore/screens/url_config/url_config_screen.dart'; -import 'package:semaphore/state/api_config.dart'; -import 'package:semaphore/state/auth.dart'; +import 'package:semaphore/screens/setting/setting_screen.dart'; +import 'package:semaphore/state/projects.dart'; +import 'package:semaphore/state/server.dart'; import 'paint_logo.dart'; @@ -18,20 +16,23 @@ class SplashScreen extends ConsumerWidget { void checkStatus(BuildContext context, WidgetRef ref) async { try { - final apiUrl = await ref.read(apiUrlProvider.notifier).loadApiUrl(); - if (apiUrl == '') { - ref.read(routerProvider).go(UrlConfigScreen.path); + final currentServer = ref.read(serversProvider.notifier).currentServer(); + if (currentServer == null) { + ref.read(routerProvider).goNamed(SettingsScreen.name, pathParameters: { + 'module': SettingsScreenModule.server.name, + }); return; } - await ref.read(userTokenProvider.notifier).readToken(); - await ref.read(userTokenProvider.notifier).checkToken(); - final isAuth = ref.read(signInStateProvider); - - ref.read(semaphoreApiProvider.notifier).rebuild(); - final url = isAuth ? HomeScreen.name : SignInScreen.name; + final currentProject = await ref.read(currentProjectProvider.future); + if (currentProject == null) { + ref.read(routerProvider).goNamed(SettingsScreen.name, pathParameters: { + 'module': SettingsScreenModule.project.name, + }); + return; + } - ref.read(routerProvider).goNamed(url); + ref.read(routerProvider).goNamed(HomeScreen.name); } catch (e) { print(e); } diff --git a/lib/state/api_config.dart b/lib/state/api_config.dart index 4866af7..a648fba 100644 --- a/lib/state/api_config.dart +++ b/lib/state/api_config.dart @@ -1,54 +1,29 @@ import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import 'shared_prefs.dart'; -import 'auth.dart'; +import 'package:semaphore/state/server.dart'; part 'api_config.g.dart'; -@riverpod -class ApiUrl extends _$ApiUrl { - static const apiUrlSaveKey = 'semaphore.url'; - @override - String build() { - ref.keepAlive(); - return ''; - } - - void changeApiUrl(String url) { - state = url; - persistentUrlConfig(url); - } - - void persistentUrlConfig(String url) async { - final prefs = await ref.read(sharedPrefsProvider.future); - prefs.setString(apiUrlSaveKey, url); - } - - Future loadApiUrl() async { - final prefs = await ref.read(sharedPrefsProvider.future); - final url = prefs.getString(apiUrlSaveKey) ?? ''; - state = url; - return url; - } -} - @riverpod class SemaphoreApi extends _$SemaphoreApi { @override AnsibleSemaphore build() { - final apiUrl = ref.read(apiUrlProvider); - final token = ref.read(userTokenProvider); - ref.keepAlive(); + final currentServer = ref.watch(serversProvider.notifier).currentServer(); + if (currentServer == null) { + return AnsibleSemaphore(); + } + final apiUrl = currentServer.apiUrl; + final token = currentServer.token; if (token != null && token.isNotEmpty && + apiUrl != null && Uri.parse(apiUrl).host.isNotEmpty) { return AnsibleSemaphore( dio: Dio(BaseOptions( baseUrl: apiUrl, headers: {'Authorization': 'Bearer $token'})), ); - } else if (Uri.parse(apiUrl).host.isNotEmpty) { + } else if (apiUrl != null && Uri.parse(apiUrl).host.isNotEmpty) { return AnsibleSemaphore( basePathOverride: apiUrl, ); @@ -56,8 +31,4 @@ class SemaphoreApi extends _$SemaphoreApi { return AnsibleSemaphore(); } } - - void rebuild() { - state = build(); - } } diff --git a/lib/state/api_config.g.dart b/lib/state/api_config.g.dart index 954aacf..d0f5b8e 100644 --- a/lib/state/api_config.g.dart +++ b/lib/state/api_config.g.dart @@ -6,21 +6,7 @@ part of 'api_config.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiUrlHash() => r'b072e672856b4d1b4e5a1f6269115397675ff12f'; - -/// See also [ApiUrl]. -@ProviderFor(ApiUrl) -final apiUrlProvider = AutoDisposeNotifierProvider.internal( - ApiUrl.new, - name: r'apiUrlProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$apiUrlHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ApiUrl = AutoDisposeNotifier; -String _$semaphoreApiHash() => r'be4c0ad29aacd40917f9077fee09d4473c2efd5a'; +String _$semaphoreApiHash() => r'118eed699331b937fa710917d0d9d2dea8e4337e'; /// See also [SemaphoreApi]. @ProviderFor(SemaphoreApi) @@ -36,4 +22,4 @@ final semaphoreApiProvider = typedef _$SemaphoreApi = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/auth.dart b/lib/state/auth.dart index b8ccfa0..668d35e 100644 --- a/lib/state/auth.dart +++ b/lib/state/auth.dart @@ -1,101 +1,15 @@ -import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:ansible_semaphore/ansible_semaphore.dart'; import 'package:semaphore/state/api_config.dart'; -import 'package:semaphore/state/shared_prefs.dart'; part 'auth.g.dart'; @riverpod -class SignInState extends _$SignInState { +class CurrentUser extends _$CurrentUser { @override - bool build() { - ref.keepAlive(); - return false; - } - - void changeToSigned() { - state = true; - } - - void changeToNotSigned() { - state = false; - } -} - -@riverpod -class SignInForm extends _$SignInForm { - @override - Login build() { - return Login(); - } - - void updateWith({ - String? auth, - String? password, - }) { - state = Login( - auth: auth ?? state.auth, - password: password ?? state.password, - ); - } -} - -@riverpod -class SignInFormError extends _$SignInFormError { - @override - DioException? build() { - return null; - } - - void updateWith(error) { - state = error; - } -} - -@riverpod -class UserToken extends _$UserToken { - static const String saveKey = 'org.gsmlg.semaphore.api_token'; - @override - String? build() { - ref.keepAlive(); - return ''; - } - - Future setToken(String? token) async { - state = token; - final secureStorage = ref.read(secureStorageProvider); - if (token != null) { - ref.read(signInStateProvider.notifier).changeToSigned(); - await secureStorage.write(key: saveKey, value: token); - } else { - ref.read(signInStateProvider.notifier).changeToNotSigned(); - await secureStorage.delete(key: saveKey); - } - } - - Future readToken() async { - final secureStorage = ref.read(secureStorageProvider); - final token = await secureStorage.read(key: saveKey); - if (token != null && token.isNotEmpty) { - state = token; - ref.read(signInStateProvider.notifier).changeToSigned(); - } else { - state = ''; - ref.read(signInStateProvider.notifier).changeToNotSigned(); - } - return token; - } - - checkToken() async { - try { - ref.read(semaphoreApiProvider.notifier).rebuild(); - final api = ref.read(semaphoreApiProvider).getProjectsApi(); - await api.projectsGet(); - ref.read(signInStateProvider.notifier).changeToSigned(); - } catch (e) { - ref.read(signInStateProvider.notifier).changeToNotSigned(); - state = ''; - } + Future build() async { + final api = ref.watch(semaphoreApiProvider).getUserApi(); + final resp = await api.userGet(); + return resp.data; } } diff --git a/lib/state/auth.g.dart b/lib/state/auth.g.dart index c237c75..a3e763a 100644 --- a/lib/state/auth.g.dart +++ b/lib/state/auth.g.dart @@ -6,66 +6,20 @@ part of 'auth.dart'; // RiverpodGenerator // ************************************************************************** -String _$signInStateHash() => r'0fdac377ebb85a07ca2b820dc9a07dee88b7e248'; - -/// See also [SignInState]. -@ProviderFor(SignInState) -final signInStateProvider = - AutoDisposeNotifierProvider.internal( - SignInState.new, - name: r'signInStateProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$signInStateHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SignInState = AutoDisposeNotifier; -String _$signInFormHash() => r'206e34dd0a7e83b9ced25226fa85fc207d24c4e4'; - -/// See also [SignInForm]. -@ProviderFor(SignInForm) -final signInFormProvider = - AutoDisposeNotifierProvider.internal( - SignInForm.new, - name: r'signInFormProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$signInFormHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SignInForm = AutoDisposeNotifier; -String _$signInFormErrorHash() => r'9ba978fbe09e61a58d48ba94a70ae4752ed272f5'; - -/// See also [SignInFormError]. -@ProviderFor(SignInFormError) -final signInFormErrorProvider = - AutoDisposeNotifierProvider.internal( - SignInFormError.new, - name: r'signInFormErrorProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$signInFormErrorHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SignInFormError = AutoDisposeNotifier; -String _$userTokenHash() => r'9aca14876a842af0f074e70fa46b4d231227e453'; - -/// See also [UserToken]. -@ProviderFor(UserToken) -final userTokenProvider = - AutoDisposeNotifierProvider.internal( - UserToken.new, - name: r'userTokenProvider', +String _$currentUserHash() => r'c8576e7fbf2f49a1c279c7b5b552c6d2e9455486'; + +/// See also [CurrentUser]. +@ProviderFor(CurrentUser) +final currentUserProvider = + AutoDisposeAsyncNotifierProvider.internal( + CurrentUser.new, + name: r'currentUserProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$userTokenHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$currentUserHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$UserToken = AutoDisposeNotifier; +typedef _$CurrentUser = AutoDisposeAsyncNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects.dart b/lib/state/projects.dart index 25774c7..6394688 100644 --- a/lib/state/projects.dart +++ b/lib/state/projects.dart @@ -8,36 +8,65 @@ part 'projects.g.dart'; class Projects extends _$Projects { @override Future> build() async { - ref.keepAlive(); try { final api = ref.read(semaphoreApiProvider).getProjectsApi(); final resp = await api.projectsGet(); - print('projects resp: $resp'); + // print('projects resp: $resp'); return resp.data ?? []; } catch (e) { return []; } } - Future getProjects() async { + Future reloadProjects() async { final api = ref.read(semaphoreApiProvider).getProjectsApi(); state = await AsyncValue.guard(() async { try { final resp = await api.projectsGet(); - print('projects resp: $resp'); + // print('projects resp: $resp'); return resp.data ?? []; } catch (e) { return []; } }); } + + Future createProject(Project project) async { + final api = ref.read(semaphoreApiProvider).getProjectsApi(); + await api.projectsPost( + project: ProjectRequest( + name: project.name, + alert: project.alert, + alertChat: project.alertChat, + maxParallelTasks: project.maxParallelTasks, + )); + reloadProjects(); + } + + Future updateProject(Project project) async { + final api = ref.read(semaphoreApiProvider).getProjectApi(); + await api.projectProjectIdPut( + projectId: project.id!, + project: ProjectRequest( + name: project.name, + alert: project.alert, + alertChat: project.alertChat, + maxParallelTasks: project.maxParallelTasks, + )); + reloadProjects(); + } + + Future deleteProject(Project project) async { + final api = ref.read(semaphoreApiProvider).getProjectApi(); + await api.projectProjectIdDelete(projectId: project.id!); + reloadProjects(); + } } @riverpod class CurrentProject extends _$CurrentProject { @override Future build() async { - ref.keepAlive(); try { final projects = await ref.read(projectsProvider.future); print('projects: $projects'); @@ -55,9 +84,38 @@ class CurrentProject extends _$CurrentProject { @riverpod class ProjectFamily extends _$ProjectFamily { @override - Future build(int projectId) async { + Future build(int? projectId) async { + if (projectId == null) { + return null; + } final api = ref.read(semaphoreApiProvider).getProjectApi(); final resp = await api.projectProjectIdGet(projectId: projectId); return resp.data; } } + +@riverpod +class ProjectFormState extends _$ProjectFormState { + @override + Project build(Project? project) { + if (project == null) { + return Project(); + } + return project; + } + + void updateWith({ + String? name, + bool? alert, + String? alertChat, + int? maxParallelTasks, + }) { + state = Project( + id: state.id, + name: name ?? state.name, + alert: alert ?? state.alert, + alertChat: alertChat ?? state.alertChat, + maxParallelTasks: maxParallelTasks ?? state.maxParallelTasks, + ); + } +} diff --git a/lib/state/projects.g.dart b/lib/state/projects.g.dart index 266c824..ed22559 100644 --- a/lib/state/projects.g.dart +++ b/lib/state/projects.g.dart @@ -6,7 +6,7 @@ part of 'projects.dart'; // RiverpodGenerator // ************************************************************************** -String _$projectsHash() => r'5a9ad436846c3f134f8bd59d340e7aa547212f6f'; +String _$projectsHash() => r'889889fcb788d623124c27b47758156b6ece81be'; /// See also [Projects]. @ProviderFor(Projects) @@ -21,7 +21,7 @@ final projectsProvider = ); typedef _$Projects = AutoDisposeAsyncNotifier>; -String _$currentProjectHash() => r'877cb7e32abb685ec867da4a009e58b097e80ff0'; +String _$currentProjectHash() => r'2b96a611a187845786d53b7708e15b9194c572b1'; /// See also [CurrentProject]. @ProviderFor(CurrentProject) @@ -37,7 +37,7 @@ final currentProjectProvider = ); typedef _$CurrentProject = AutoDisposeAsyncNotifier; -String _$projectFamilyHash() => r'56565a73dd8d77cca0c78238f491876ae9516653'; +String _$projectFamilyHash() => r'09db0c3cc30e2233da78d1020ca278a244c82b54'; /// Copied from Dart SDK class _SystemHash { @@ -62,10 +62,10 @@ class _SystemHash { abstract class _$ProjectFamily extends BuildlessAutoDisposeAsyncNotifier { - late final int projectId; + late final int? projectId; Future build( - int projectId, + int? projectId, ); } @@ -80,7 +80,7 @@ class ProjectFamilyFamily extends Family> { /// See also [ProjectFamily]. ProjectFamilyProvider call( - int projectId, + int? projectId, ) { return ProjectFamilyProvider( projectId, @@ -116,8 +116,8 @@ class ProjectFamilyProvider extends AutoDisposeAsyncNotifierProviderImpl { /// See also [ProjectFamily]. ProjectFamilyProvider( - this.projectId, - ) : super.internal( + int? projectId, + ) : this._internal( () => ProjectFamily()..projectId = projectId, from: projectFamilyProvider, name: r'projectFamilyProvider', @@ -128,9 +128,51 @@ class ProjectFamilyProvider dependencies: ProjectFamilyFamily._dependencies, allTransitiveDependencies: ProjectFamilyFamily._allTransitiveDependencies, + projectId: projectId, ); - final int projectId; + ProjectFamilyProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.projectId, + }) : super.internal(); + + final int? projectId; + + @override + Future runNotifierBuild( + covariant ProjectFamily notifier, + ) { + return notifier.build( + projectId, + ); + } + + @override + Override overrideWith(ProjectFamily Function() create) { + return ProviderOverride( + origin: this, + override: ProjectFamilyProvider._internal( + () => create()..projectId = projectId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + projectId: projectId, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _ProjectFamilyProviderElement(this); + } @override bool operator ==(Object other) { @@ -144,15 +186,164 @@ class ProjectFamilyProvider return _SystemHash.finish(hash); } +} + +mixin ProjectFamilyRef on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `projectId` of this provider. + int? get projectId; +} + +class _ProjectFamilyProviderElement + extends AutoDisposeAsyncNotifierProviderElement + with ProjectFamilyRef { + _ProjectFamilyProviderElement(super.provider); @override - Future runNotifierBuild( - covariant ProjectFamily notifier, + int? get projectId => (origin as ProjectFamilyProvider).projectId; +} + +String _$projectFormStateHash() => r'1591ff1ccf537f8a328a13617bd558ae7322d192'; + +abstract class _$ProjectFormState + extends BuildlessAutoDisposeNotifier { + late final Project? project; + + Project build( + Project? project, + ); +} + +/// See also [ProjectFormState]. +@ProviderFor(ProjectFormState) +const projectFormStateProvider = ProjectFormStateFamily(); + +/// See also [ProjectFormState]. +class ProjectFormStateFamily extends Family { + /// See also [ProjectFormState]. + const ProjectFormStateFamily(); + + /// See also [ProjectFormState]. + ProjectFormStateProvider call( + Project? project, + ) { + return ProjectFormStateProvider( + project, + ); + } + + @override + ProjectFormStateProvider getProviderOverride( + covariant ProjectFormStateProvider provider, + ) { + return call( + provider.project, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'projectFormStateProvider'; +} + +/// See also [ProjectFormState]. +class ProjectFormStateProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ProjectFormState]. + ProjectFormStateProvider( + Project? project, + ) : this._internal( + () => ProjectFormState()..project = project, + from: projectFormStateProvider, + name: r'projectFormStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$projectFormStateHash, + dependencies: ProjectFormStateFamily._dependencies, + allTransitiveDependencies: + ProjectFormStateFamily._allTransitiveDependencies, + project: project, + ); + + ProjectFormStateProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.project, + }) : super.internal(); + + final Project? project; + + @override + Project runNotifierBuild( + covariant ProjectFormState notifier, ) { return notifier.build( - projectId, + project, ); } + + @override + Override overrideWith(ProjectFormState Function() create) { + return ProviderOverride( + origin: this, + override: ProjectFormStateProvider._internal( + () => create()..project = project, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + project: project, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _ProjectFormStateProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ProjectFormStateProvider && other.project == project; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, project.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ProjectFormStateRef on AutoDisposeNotifierProviderRef { + /// The parameter `project` of this provider. + Project? get project; +} + +class _ProjectFormStateProviderElement + extends AutoDisposeNotifierProviderElement + with ProjectFormStateRef { + _ProjectFormStateProviderElement(super.provider); + + @override + Project? get project => (origin as ProjectFormStateProvider).project; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/access_key.dart b/lib/state/projects/access_key.dart index 94b09ef..b49bb6b 100644 --- a/lib/state/projects/access_key.dart +++ b/lib/state/projects/access_key.dart @@ -1,5 +1,4 @@ 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'; @@ -8,6 +7,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/access_key/form.dart'; import 'package:semaphore/state/api_config.dart'; @@ -44,7 +44,7 @@ class AccessKeyDataTable extends BaseGridData { context: context, child: AccessKeyForm(accessKeyId: accessKey.id)); }, - iconData: (Icons.edit)), + icon: const AdaptiveIcon(Icons.edit)), AdaptiveIconButton( onPressed: () { adaptiveAlertDialog( @@ -53,7 +53,7 @@ class AccessKeyDataTable extends BaseGridData { content: const Text( 'Are you sure you want to delete this accessKey?'), primaryButton: AdaptiveButton( - controlSize: ControlSize.large, + color: Colors.redAccent, onPressed: () async { final api = ref.read(semaphoreApiProvider).getProjectApi(); @@ -66,10 +66,8 @@ class AccessKeyDataTable extends BaseGridData { } ref.read(accessKeyListProvider.notifier).loadRows(); }, - child: const Text('Delete', - style: TextStyle(color: Colors.red))), + child: const Text('Delete')), secondaryButton: AdaptiveButton( - controlSize: ControlSize.large, onPressed: () { Navigator.of(context).pop(); }, @@ -77,7 +75,7 @@ class AccessKeyDataTable extends BaseGridData { ), ); }, - iconData: (Icons.delete)), + icon: const AdaptiveIcon(Icons.delete)), ], ); }); diff --git a/lib/state/projects/access_key.g.dart b/lib/state/projects/access_key.g.dart index 744287f..806d0af 100644 --- a/lib/state/projects/access_key.g.dart +++ b/lib/state/projects/access_key.g.dart @@ -116,8 +116,8 @@ class AccessKeyFormRequestProvider extends AutoDisposeNotifierProviderImpl< AccessKeyFormRequest, AccessKeyRequest> { /// See also [AccessKeyFormRequest]. AccessKeyFormRequestProvider( - this.item, - ) : super.internal( + AccessKey? item, + ) : this._internal( () => AccessKeyFormRequest()..item = item, from: accessKeyFormRequestProvider, name: r'accessKeyFormRequestProvider', @@ -128,10 +128,52 @@ class AccessKeyFormRequestProvider extends AutoDisposeNotifierProviderImpl< dependencies: AccessKeyFormRequestFamily._dependencies, allTransitiveDependencies: AccessKeyFormRequestFamily._allTransitiveDependencies, + item: item, ); + AccessKeyFormRequestProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + final AccessKey? item; + @override + AccessKeyRequest runNotifierBuild( + covariant AccessKeyFormRequest notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(AccessKeyFormRequest Function() create) { + return ProviderOverride( + origin: this, + override: AccessKeyFormRequestProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _AccessKeyFormRequestProviderElement(this); + } + @override bool operator ==(Object other) { return other is AccessKeyFormRequestProvider && other.item == item; @@ -144,15 +186,21 @@ class AccessKeyFormRequestProvider extends AutoDisposeNotifierProviderImpl< return _SystemHash.finish(hash); } +} + +mixin AccessKeyFormRequestRef + on AutoDisposeNotifierProviderRef { + /// The parameter `item` of this provider. + AccessKey? get item; +} + +class _AccessKeyFormRequestProviderElement + extends AutoDisposeNotifierProviderElement with AccessKeyFormRequestRef { + _AccessKeyFormRequestProviderElement(super.provider); @override - AccessKeyRequest runNotifierBuild( - covariant AccessKeyFormRequest notifier, - ) { - return notifier.build( - item, - ); - } + AccessKey? get item => (origin as AccessKeyFormRequestProvider).item; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/activity.dart b/lib/state/projects/activity.dart new file mode 100644 index 0000000..c98b80f --- /dev/null +++ b/lib/state/projects/activity.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:ansible_semaphore/ansible_semaphore.dart'; +import 'package:pluto_grid/pluto_grid.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:semaphore/adaptive/text_timeago.dart'; +import 'package:semaphore/state/api_config.dart'; +import 'package:semaphore/state/projects.dart'; +import 'package:semaphore/utils/base_griddata.dart'; + +part 'activity.g.dart'; + +class ActivityDataTable extends BaseGridData { + @override + final List columns = [ + PlutoColumn( + title: 'Time', + field: 'time', + type: PlutoColumnType.text(), + renderer: (PlutoColumnRendererContext renderContext) { + final DateTime? value = renderContext.cell.value; + return AdaptiveTextTimeago(value); + }, + ), + PlutoColumn( + title: 'User', + field: 'user', + type: PlutoColumnType.text(), + ), + PlutoColumn( + title: 'Description', + field: 'description', + type: PlutoColumnType.text(), + ), + ]; + + ActivityDataTable({ + required super.rows, + super.stateManager, + super.configuration = const PlutoGridConfiguration(), + }); + + ActivityDataTable copyWith({ + List? rows, + PlutoGridStateManager? stateManager, + PlutoGridConfiguration? configuration, + }) { + return ActivityDataTable( + rows: rows ?? this.rows, + stateManager: stateManager ?? this.stateManager, + configuration: configuration ?? this.configuration, + ); + } + + @override + List mapData(List data) { + return data + .map((rowData) => PlutoRow( + cells: { + 'time': PlutoCell(value: rowData.created), + 'user': PlutoCell(value: rowData.username), + 'description': PlutoCell(value: rowData.description), + }, + )) + .toList(); + } +} + +@riverpod +class ActivityList extends _$ActivityList { + Timer? timer; + + @override + ActivityDataTable build() { + return ActivityDataTable(rows: []); + } + + void setStateManager(PlutoGridStateManager stateManager) { + state = state.copyWith(stateManager: stateManager); + loadRows(); + timer?.cancel(); + timer = Timer.periodic(const Duration(seconds: 5), (timer) { + loadRows(noRefresh: true); + }); + ref.onDispose(() { + timer?.cancel(); + }); + } + + Future loadRows({bool? noRefresh}) async { + if (noRefresh != true) state.stateManager!.setShowLoading(true); + final data = await loadData(); + + final rows = state.mapData(data); + final newRows = PlutoGridStateManager.initializeRows(state.columns, rows); + + state = state.copyWith(rows: newRows); + state.stateManager!.refRows.clear(); + state.stateManager!.refRows.addAll(newRows); + if (noRefresh != true) state.stateManager!.setShowLoading(false); + } + + Future> loadData() async { + final api = ref.read(semaphoreApiProvider).getProjectApi(); + final current = await ref.read(currentProjectProvider.future); + final resp = + await api.projectProjectIdEventsLastGet(projectId: current!.id!); + return resp.data ?? []; + } +} diff --git a/lib/state/projects/activity.g.dart b/lib/state/projects/activity.g.dart new file mode 100644 index 0000000..176a7e5 --- /dev/null +++ b/lib/state/projects/activity.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$activityListHash() => r'58aa5e2dfb67b6fc8ececa0e0d4a2813cb4b7b21'; + +/// See also [ActivityList]. +@ProviderFor(ActivityList) +final activityListProvider = + AutoDisposeNotifierProvider.internal( + ActivityList.new, + name: r'activityListProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$activityListHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ActivityList = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/environment.dart b/lib/state/projects/environment.dart index aa3aa4e..7db3288 100644 --- a/lib/state/projects/environment.dart +++ b/lib/state/projects/environment.dart @@ -6,6 +6,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/environment/form.dart'; import 'package:semaphore/state/api_config.dart'; @@ -39,7 +40,7 @@ class EnvironmentDataTable extends BaseGridData { child: EnvironmentForm(environmentId: environment.id), ); }, - iconData: Icons.edit, + icon: const AdaptiveIcon(Icons.edit), ), AdaptiveIconButton( onPressed: () { @@ -55,7 +56,7 @@ class EnvironmentDataTable extends BaseGridData { }, child: const Text('Cancel')), primaryButton: AdaptiveButton( - controlSize: ControlSize.large, + color: Colors.redAccent, onPressed: () async { final api = ref.read(semaphoreApiProvider).getProjectApi(); @@ -72,11 +73,10 @@ class EnvironmentDataTable extends BaseGridData { .read(environmentListProvider.notifier) .loadRows(); }, - child: const Text('Delete', - style: TextStyle(color: Colors.red))), + child: const Text('Delete')), ); }, - iconData: Icons.delete), + icon: const AdaptiveIcon(Icons.delete)), ], ); }); diff --git a/lib/state/projects/environment.g.dart b/lib/state/projects/environment.g.dart index 86e018b..6d547c9 100644 --- a/lib/state/projects/environment.g.dart +++ b/lib/state/projects/environment.g.dart @@ -117,8 +117,8 @@ class EnvironmentFormRequestProvider extends AutoDisposeNotifierProviderImpl< EnvironmentFormRequest, EnvironmentRequest> { /// See also [EnvironmentFormRequest]. EnvironmentFormRequestProvider( - this.item, - ) : super.internal( + Environment? item, + ) : this._internal( () => EnvironmentFormRequest()..item = item, from: environmentFormRequestProvider, name: r'environmentFormRequestProvider', @@ -129,10 +129,52 @@ class EnvironmentFormRequestProvider extends AutoDisposeNotifierProviderImpl< dependencies: EnvironmentFormRequestFamily._dependencies, allTransitiveDependencies: EnvironmentFormRequestFamily._allTransitiveDependencies, + item: item, ); + EnvironmentFormRequestProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + final Environment? item; + @override + EnvironmentRequest runNotifierBuild( + covariant EnvironmentFormRequest notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(EnvironmentFormRequest Function() create) { + return ProviderOverride( + origin: this, + override: EnvironmentFormRequestProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _EnvironmentFormRequestProviderElement(this); + } + @override bool operator ==(Object other) { return other is EnvironmentFormRequestProvider && other.item == item; @@ -145,15 +187,21 @@ class EnvironmentFormRequestProvider extends AutoDisposeNotifierProviderImpl< return _SystemHash.finish(hash); } +} + +mixin EnvironmentFormRequestRef + on AutoDisposeNotifierProviderRef { + /// The parameter `item` of this provider. + Environment? get item; +} + +class _EnvironmentFormRequestProviderElement + extends AutoDisposeNotifierProviderElement with EnvironmentFormRequestRef { + _EnvironmentFormRequestProviderElement(super.provider); @override - EnvironmentRequest runNotifierBuild( - covariant EnvironmentFormRequest notifier, - ) { - return notifier.build( - item, - ); - } + Environment? get item => (origin as EnvironmentFormRequestProvider).item; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/inventory.dart b/lib/state/projects/inventory.dart index ef210d3..d86060e 100644 --- a/lib/state/projects/inventory.dart +++ b/lib/state/projects/inventory.dart @@ -9,6 +9,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/inventory/form.dart'; import 'package:semaphore/state/api_config.dart'; @@ -66,7 +67,7 @@ class InventoryDataTable extends BaseGridData { child: InventoryForm(inventoryId: inventory.id), ); }, - iconData: (Icons.edit)), + icon: const AdaptiveIcon(Icons.edit)), AdaptiveIconButton( onPressed: () { adaptiveAlertDialog( @@ -80,6 +81,7 @@ class InventoryDataTable extends BaseGridData { }, child: const Text('Cancel')), primaryButton: AdaptiveButton( + color: Colors.redAccent, onPressed: () async { final api = ref .read(semaphoreApiProvider) @@ -97,11 +99,10 @@ class InventoryDataTable extends BaseGridData { .read(inventoryListProvider.notifier) .loadRows(); }, - child: const Text('Delete', - style: TextStyle(color: Colors.red))), + child: const Text('Delete')), ); }, - iconData: (Icons.delete)), + icon: const AdaptiveIcon(Icons.delete)), ], ); }); diff --git a/lib/state/projects/inventory.g.dart b/lib/state/projects/inventory.g.dart index d143ea7..f7d96f5 100644 --- a/lib/state/projects/inventory.g.dart +++ b/lib/state/projects/inventory.g.dart @@ -116,8 +116,8 @@ class InventoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< InventoryFormRequest, InventoryRequest> { /// See also [InventoryFormRequest]. InventoryFormRequestProvider( - this.item, - ) : super.internal( + Inventory? item, + ) : this._internal( () => InventoryFormRequest()..item = item, from: inventoryFormRequestProvider, name: r'inventoryFormRequestProvider', @@ -128,10 +128,52 @@ class InventoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< dependencies: InventoryFormRequestFamily._dependencies, allTransitiveDependencies: InventoryFormRequestFamily._allTransitiveDependencies, + item: item, ); + InventoryFormRequestProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + final Inventory? item; + @override + InventoryRequest runNotifierBuild( + covariant InventoryFormRequest notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(InventoryFormRequest Function() create) { + return ProviderOverride( + origin: this, + override: InventoryFormRequestProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _InventoryFormRequestProviderElement(this); + } + @override bool operator ==(Object other) { return other is InventoryFormRequestProvider && other.item == item; @@ -144,15 +186,21 @@ class InventoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< return _SystemHash.finish(hash); } +} + +mixin InventoryFormRequestRef + on AutoDisposeNotifierProviderRef { + /// The parameter `item` of this provider. + Inventory? get item; +} + +class _InventoryFormRequestProviderElement + extends AutoDisposeNotifierProviderElement with InventoryFormRequestRef { + _InventoryFormRequestProviderElement(super.provider); @override - InventoryRequest runNotifierBuild( - covariant InventoryFormRequest notifier, - ) { - return notifier.build( - item, - ); - } + Inventory? get item => (origin as InventoryFormRequestProvider).item; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/repository.dart b/lib/state/projects/repository.dart index b97e77a..dce95f3 100644 --- a/lib/state/projects/repository.dart +++ b/lib/state/projects/repository.dart @@ -6,6 +6,7 @@ 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/access_key_name.dart'; import 'package:semaphore/components/repository/form.dart'; @@ -53,7 +54,7 @@ class RepositoryDataTable extends BaseGridData { child: RepositoryForm(repositoryId: repository.id), ); }, - iconData: (Icons.edit)), + icon: const AdaptiveIcon(Icons.edit)), AdaptiveIconButton( onPressed: () { adaptiveAlertDialog( @@ -67,6 +68,7 @@ class RepositoryDataTable extends BaseGridData { }, child: const Text('Cancel')), primaryButton: AdaptiveButton( + color: Colors.redAccent, onPressed: () async { final api = ref.read(semaphoreApiProvider).getProjectApi(); @@ -83,11 +85,10 @@ class RepositoryDataTable extends BaseGridData { .read(repositoryListProvider.notifier) .loadRows(); }, - child: const Text('Delete', - style: TextStyle(color: Colors.red))), + child: const Text('Delete')), ); }, - iconData: (Icons.delete)), + icon: const AdaptiveIcon(Icons.delete)), ], ); }); diff --git a/lib/state/projects/repository.g.dart b/lib/state/projects/repository.g.dart index 7e0495d..643f553 100644 --- a/lib/state/projects/repository.g.dart +++ b/lib/state/projects/repository.g.dart @@ -116,8 +116,8 @@ class RepositoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< RepositoryFormRequest, RepositoryRequest> { /// See also [RepositoryFormRequest]. RepositoryFormRequestProvider( - this.item, - ) : super.internal( + Repository? item, + ) : this._internal( () => RepositoryFormRequest()..item = item, from: repositoryFormRequestProvider, name: r'repositoryFormRequestProvider', @@ -128,10 +128,52 @@ class RepositoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< dependencies: RepositoryFormRequestFamily._dependencies, allTransitiveDependencies: RepositoryFormRequestFamily._allTransitiveDependencies, + item: item, ); + RepositoryFormRequestProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + final Repository? item; + @override + RepositoryRequest runNotifierBuild( + covariant RepositoryFormRequest notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(RepositoryFormRequest Function() create) { + return ProviderOverride( + origin: this, + override: RepositoryFormRequestProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _RepositoryFormRequestProviderElement(this); + } + @override bool operator ==(Object other) { return other is RepositoryFormRequestProvider && other.item == item; @@ -144,15 +186,21 @@ class RepositoryFormRequestProvider extends AutoDisposeNotifierProviderImpl< return _SystemHash.finish(hash); } +} + +mixin RepositoryFormRequestRef + on AutoDisposeNotifierProviderRef { + /// The parameter `item` of this provider. + Repository? get item; +} + +class _RepositoryFormRequestProviderElement + extends AutoDisposeNotifierProviderElement with RepositoryFormRequestRef { + _RepositoryFormRequestProviderElement(super.provider); @override - RepositoryRequest runNotifierBuild( - covariant RepositoryFormRequest notifier, - ) { - return notifier.build( - item, - ); - } + Repository? get item => (origin as RepositoryFormRequestProvider).item; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/task.dart b/lib/state/projects/task.dart index e66a496..aaef38c 100644 --- a/lib/state/projects/task.dart +++ b/lib/state/projects/task.dart @@ -204,13 +204,13 @@ Future> taskOutput(TaskOutputRef ref, int id) async { } @riverpod -Stream> taskOutputStream( +Stream<(Task, List)> taskOutputStream( TaskOutputStreamRef ref, int taskId) async* { Task task; do { task = await ref.read(taskFamilyProvider(taskId).future); final output = await ref.read(taskOutputProvider(taskId).future); - yield output; + yield (task, output); await Future.delayed(const Duration(seconds: 1)); } while (task.status == 'waiting' || task.status == 'running'); } diff --git a/lib/state/projects/task.g.dart b/lib/state/projects/task.g.dart index 1c5afe9..7418f35 100644 --- a/lib/state/projects/task.g.dart +++ b/lib/state/projects/task.g.dart @@ -29,8 +29,6 @@ class _SystemHash { } } -typedef TaskFamilyRef = AutoDisposeFutureProviderRef; - /// See also [taskFamily]. @ProviderFor(taskFamily) const taskFamilyProvider = TaskFamilyFamily(); @@ -77,10 +75,10 @@ class TaskFamilyFamily extends Family> { class TaskFamilyProvider extends AutoDisposeFutureProvider { /// See also [taskFamily]. TaskFamilyProvider( - this.id, - ) : super.internal( + int id, + ) : this._internal( (ref) => taskFamily( - ref, + ref as TaskFamilyRef, id, ), from: taskFamilyProvider, @@ -92,10 +90,44 @@ class TaskFamilyProvider extends AutoDisposeFutureProvider { dependencies: TaskFamilyFamily._dependencies, allTransitiveDependencies: TaskFamilyFamily._allTransitiveDependencies, + id: id, ); + TaskFamilyProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + final int id; + @override + Override overrideWith( + FutureOr Function(TaskFamilyRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TaskFamilyProvider._internal( + (ref) => create(ref as TaskFamilyRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _TaskFamilyProviderElement(this); + } + @override bool operator ==(Object other) { return other is TaskFamilyProvider && other.id == id; @@ -110,8 +142,20 @@ class TaskFamilyProvider extends AutoDisposeFutureProvider { } } +mixin TaskFamilyRef on AutoDisposeFutureProviderRef { + /// The parameter `id` of this provider. + int get id; +} + +class _TaskFamilyProviderElement extends AutoDisposeFutureProviderElement + with TaskFamilyRef { + _TaskFamilyProviderElement(super.provider); + + @override + int get id => (origin as TaskFamilyProvider).id; +} + String _$taskOutputHash() => r'941cc24b5817823d370bc488d41106103efcc45d'; -typedef TaskOutputRef = AutoDisposeFutureProviderRef>; /// See also [taskOutput]. @ProviderFor(taskOutput) @@ -159,10 +203,10 @@ class TaskOutputFamily extends Family>> { class TaskOutputProvider extends AutoDisposeFutureProvider> { /// See also [taskOutput]. TaskOutputProvider( - this.id, - ) : super.internal( + int id, + ) : this._internal( (ref) => taskOutput( - ref, + ref as TaskOutputRef, id, ), from: taskOutputProvider, @@ -174,10 +218,44 @@ class TaskOutputProvider extends AutoDisposeFutureProvider> { dependencies: TaskOutputFamily._dependencies, allTransitiveDependencies: TaskOutputFamily._allTransitiveDependencies, + id: id, ); + TaskOutputProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + final int id; + @override + Override overrideWith( + FutureOr> Function(TaskOutputRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TaskOutputProvider._internal( + (ref) => create(ref as TaskOutputRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _TaskOutputProviderElement(this); + } + @override bool operator ==(Object other) { return other is TaskOutputProvider && other.id == id; @@ -192,15 +270,29 @@ class TaskOutputProvider extends AutoDisposeFutureProvider> { } } -String _$taskOutputStreamHash() => r'ad50955a6d358fefb5877a09754050c445cad475'; -typedef TaskOutputStreamRef = AutoDisposeStreamProviderRef>; +mixin TaskOutputRef on AutoDisposeFutureProviderRef> { + /// The parameter `id` of this provider. + int get id; +} + +class _TaskOutputProviderElement + extends AutoDisposeFutureProviderElement> + with TaskOutputRef { + _TaskOutputProviderElement(super.provider); + + @override + int get id => (origin as TaskOutputProvider).id; +} + +String _$taskOutputStreamHash() => r'bbdffab0b83bbd4824ad3cbca5d2e16bf3343c18'; /// See also [taskOutputStream]. @ProviderFor(taskOutputStream) const taskOutputStreamProvider = TaskOutputStreamFamily(); /// See also [taskOutputStream]. -class TaskOutputStreamFamily extends Family>> { +class TaskOutputStreamFamily + extends Family)>> { /// See also [taskOutputStream]. const TaskOutputStreamFamily(); @@ -239,13 +331,13 @@ class TaskOutputStreamFamily extends Family>> { /// See also [taskOutputStream]. class TaskOutputStreamProvider - extends AutoDisposeStreamProvider> { + extends AutoDisposeStreamProvider<(Task, List)> { /// See also [taskOutputStream]. TaskOutputStreamProvider( - this.taskId, - ) : super.internal( + int taskId, + ) : this._internal( (ref) => taskOutputStream( - ref, + ref as TaskOutputStreamRef, taskId, ), from: taskOutputStreamProvider, @@ -257,10 +349,45 @@ class TaskOutputStreamProvider dependencies: TaskOutputStreamFamily._dependencies, allTransitiveDependencies: TaskOutputStreamFamily._allTransitiveDependencies, + taskId: taskId, ); + TaskOutputStreamProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.taskId, + }) : super.internal(); + final int taskId; + @override + Override overrideWith( + Stream<(Task, List)> Function(TaskOutputStreamRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: TaskOutputStreamProvider._internal( + (ref) => create(ref as TaskOutputStreamRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + taskId: taskId, + ), + ); + } + + @override + AutoDisposeStreamProviderElement<(Task, List)> createElement() { + return _TaskOutputStreamProviderElement(this); + } + @override bool operator ==(Object other) { return other is TaskOutputStreamProvider && other.taskId == taskId; @@ -275,7 +402,22 @@ class TaskOutputStreamProvider } } -String _$taskListHash() => r'117a7f642010f111b30261e917874e02a8f4c3a7'; +mixin TaskOutputStreamRef + on AutoDisposeStreamProviderRef<(Task, List)> { + /// The parameter `taskId` of this provider. + int get taskId; +} + +class _TaskOutputStreamProviderElement + extends AutoDisposeStreamProviderElement<(Task, List)> + with TaskOutputStreamRef { + _TaskOutputStreamProviderElement(super.provider); + + @override + int get taskId => (origin as TaskOutputStreamProvider).taskId; +} + +String _$taskListHash() => r'c953051e136b9c07dbae4ad7ae793949f2220095'; /// See also [TaskList]. @ProviderFor(TaskList) @@ -291,4 +433,4 @@ final taskListProvider = typedef _$TaskList = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/state/projects/template.dart b/lib/state/projects/template.dart index 72abbea..bdb5aeb 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.dart'; import 'package:semaphore/adaptive/icon_button.dart'; import 'package:semaphore/components/environment_name.dart'; import 'package:semaphore/components/inventory_name.dart'; @@ -85,7 +86,7 @@ class TemplateDataTable extends BaseGridData