diff --git a/lib/providers/environment_providers.dart b/lib/providers/environment_providers.dart index b431930ba..79e63196f 100644 --- a/lib/providers/environment_providers.dart +++ b/lib/providers/environment_providers.dart @@ -78,6 +78,7 @@ class EnvironmentsStateNotifier id: kGlobalEnvironmentId, name: "Global", values: [], + color: null, // Default global environment has no color ); state = { kGlobalEnvironmentId: globalEnvironment, @@ -95,6 +96,7 @@ class EnvironmentsStateNotifier id: environmentModelFromJson.id, name: environmentModelFromJson.name, values: environmentModelFromJson.values, + color: environmentModelFromJson.color, // Load color ); environmentsMap[environmentId] = environmentModel; } @@ -104,11 +106,12 @@ class EnvironmentsStateNotifier } } - void addEnvironment() { + void addEnvironment({String? color}) { final id = getNewUuid(); final newEnvironmentModel = EnvironmentModel( id: id, values: [], + color: _validateColor(color), // Validate and set color ); state = { ...state!, @@ -126,11 +129,13 @@ class EnvironmentsStateNotifier String id, { String? name, List? values, + String? color, }) { final environment = state![id]!; final updatedEnvironment = environment.copyWith( name: name ?? environment.name, values: values ?? environment.values, + color: _validateColor(color) ?? environment.color, ); state = { ...state!, @@ -146,6 +151,7 @@ class EnvironmentsStateNotifier final newEnvironment = environment.copyWith( id: newId, name: "${environment.name} Copy", + color: environment.color, // Copy the same color ); var environmentIds = ref.read(environmentSequenceProvider); @@ -208,4 +214,14 @@ class EnvironmentsStateNotifier ref.read(saveDataStateProvider.notifier).state = false; ref.read(hasUnsavedChangesProvider.notifier).state = false; } + + // Validates color as a hex string + String? _validateColor(String? color) { + if (color == null) return null; + final hexColor = color.startsWith('#') ? color.substring(1) : color; + if (RegExp(r'^[0-9A-Fa-f]{6}$').hasMatch(hexColor)) { + return '#$hexColor'; + } + return null; + } } diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index e68b01fb4..5eb81b0b9 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -19,3 +19,4 @@ export 'envvar_span.dart'; export 'sidebar_filter.dart'; export 'sidebar_header.dart'; export 'sidebar_save_button.dart'; +export 'env_editor_title_actions.dart'; \ No newline at end of file diff --git a/lib/screens/common_widgets/env_editor_title_actions.dart b/lib/screens/common_widgets/env_editor_title_actions.dart new file mode 100644 index 000000000..085c7364d --- /dev/null +++ b/lib/screens/common_widgets/env_editor_title_actions.dart @@ -0,0 +1,119 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/color_picker_dialog.dart'; + +class EnvEditorTitleActions extends ConsumerWidget { + const EnvEditorTitleActions({ + super.key, + this.onRenamePressed, + this.onDuplicatePressed, + this.onDeletePressed, + }); + + final void Function()? onRenamePressed; + final void Function()? onDuplicatePressed; + final void Function()? onDeletePressed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + var verticalDivider = VerticalDivider( + width: 2, + color: Theme.of(context).colorScheme.outlineVariant, + ); + final environmentId = ref.watch(selectedEnvironmentIdStateProvider); + final environmentColor = ref + .watch(selectedEnvironmentModelProvider.select((value) => value?.color)); + + return ClipRRect( + borderRadius: kBorderRadius20, + child: Material( + color: Colors.transparent, + child: Ink( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + borderRadius: kBorderRadius20, + ), + child: SizedBox( + height: 32, + child: IntrinsicHeight( + child: Row( + children: [ + iconButton( + "Rename", + Icons.edit_rounded, + onRenamePressed, + padding: const EdgeInsets.only(left: 4), + ), + verticalDivider, + iconButton( + "Pick Color", + Icons.color_lens_rounded, + environmentId != null + ? () async { + final initialColor = environmentColor != null + ? Color(int.parse( + environmentColor.substring(1), + radix: 16)) + : null; + final selectedColor = await showColorPickerDialog( + context, + initialColor: initialColor, + ); + if (selectedColor != null) { + ref + .read(environmentsStateNotifierProvider.notifier) + .updateEnvironment( + environmentId, + color: selectedColor, + ); + } + } + : null, + padding: const EdgeInsets.all(0), + ), + verticalDivider, + iconButton( + "Delete", + Icons.delete_rounded, + onDeletePressed, + ), + verticalDivider, + iconButton( + "Duplicate", + Icons.copy_rounded, + onDuplicatePressed, + padding: const EdgeInsets.only(right: 4), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget iconButton( + String tooltip, IconData iconData, void Function()? onPressed, + {EdgeInsets padding = const EdgeInsets.all(0)}) { + return Tooltip( + message: tooltip, + child: IconButton( + style: ButtonStyle( + padding: WidgetStateProperty.all(const EdgeInsets.all(0) + padding), + shape: WidgetStateProperty.all(const ContinuousRectangleBorder()), + ), + onPressed: onPressed, + icon: Icon( + iconData, + size: 16, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index 4ca07b570..385fdd2cf 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -40,7 +40,7 @@ class EnvironmentEditor extends ConsumerWidget { const SizedBox( width: 6, ), - EditorTitleActions( + EnvEditorTitleActions( // Replaced EditorTitleActions onRenamePressed: () { showRenameDialog(context, "Rename Environment", name, (val) { diff --git a/lib/widgets/color_picker_dialog.dart b/lib/widgets/color_picker_dialog.dart new file mode 100644 index 000000000..06ae30d0b --- /dev/null +++ b/lib/widgets/color_picker_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + +Future showColorPickerDialog( + BuildContext context, { + Color? initialColor, +}) async { + Color selectedColor = initialColor ?? Colors.white; + bool confirmed = false; + + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Pick a color'), + content: SingleChildScrollView( + child: ColorPicker( + pickerColor: selectedColor, + onColorChanged: (color) { + selectedColor = color; + }, + showLabel: true, + pickerAreaHeightPercent: 0.8, + enableAlpha: true, // transparency isn’t need + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + confirmed = true; + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ), + ); + + if (confirmed) { + return '#${selectedColor.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; + } + return null; +} \ No newline at end of file diff --git a/lib/widgets/popup_menu_env.dart b/lib/widgets/popup_menu_env.dart index 320269545..c68945fcf 100644 --- a/lib/widgets/popup_menu_env.dart +++ b/lib/widgets/popup_menu_env.dart @@ -16,27 +16,81 @@ class EnvironmentPopupMenu extends StatelessWidget { final void Function(EnvironmentModel? value)? onChanged; final List? options; + Color? _parseColor(String? hexColor) { + if (hexColor == null) return null; + return Color(int.parse(hexColor.substring(1), radix: 16)).withOpacity(0.2); + } + + Color? _getTextColor(Color? bgColor) { + if (bgColor == null) return null; + return bgColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; + } + @override Widget build(BuildContext context) { - final double width = context.isCompactWindow ? 100 : 130; + final double width = context.isCompactWindow ? 100 : 130; // Reintroduced dynamic width - return ADPopupMenu( - value: value == null - ? "Select Env." - : value?.id == kGlobalEnvironmentId - ? "Global" - : getEnvironmentTitle(value?.name), - values: options?.map((e) => ( - e, - (e.id == kGlobalEnvironmentId) - ? "Global" - : getEnvironmentTitle(e.name).clip(30) - )) ?? - [], - width: width, + // Use the active environment's color directly for the dropdown button + final Color? activeBgColor = _parseColor(value?.color); + return PopupMenuButton( tooltip: "Select Environment", - onChanged: onChanged, - isOutlined: true, + surfaceTintColor: kColorTransparent, + constraints: BoxConstraints(minWidth: width), + itemBuilder: (BuildContext context) => options?.map((e) { + final label = (e.id == kGlobalEnvironmentId) + ? "None" + : getEnvironmentTitle(e.name).clip(30); + final Color? bgColor = _parseColor(e.color); + return PopupMenuItem( + value: e, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: bgColor?.withOpacity(0.2), + borderRadius: kBorderRadius8, + ), + child: Text( + label, + style: kTextStylePopupMenuItem.copyWith( + color: _getTextColor(bgColor), + ), + ), + ), + ); + }).toList() ?? + [], + onSelected: onChanged, + child: Container( + width: width, + padding: kP8, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius8, + color: activeBgColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + (value == null || value?.id == kGlobalEnvironmentId) + ? "None" + : getEnvironmentTitle(value?.name), + style: kTextStylePopupMenuItem.copyWith( + color: _getTextColor(activeBgColor), + ), + ), + ), + const Icon( + Icons.unfold_more, + size: 16, + ), + ], + ), + ), ); } } diff --git a/packages/apidash_core/lib/models/environment_model.dart b/packages/apidash_core/lib/models/environment_model.dart index 9242d4d59..c19a58c2e 100644 --- a/packages/apidash_core/lib/models/environment_model.dart +++ b/packages/apidash_core/lib/models/environment_model.dart @@ -15,6 +15,7 @@ class EnvironmentModel with _$EnvironmentModel { required String id, @Default("") String name, @Default([]) List values, + String? color, // New field for environment color (hex string, e.g., #FF0000) }) = _EnvironmentModel; factory EnvironmentModel.fromJson(Map json) => diff --git a/packages/apidash_core/lib/models/environment_model.freezed.dart b/packages/apidash_core/lib/models/environment_model.freezed.dart index a21c814cc..d85f0ebb9 100644 --- a/packages/apidash_core/lib/models/environment_model.freezed.dart +++ b/packages/apidash_core/lib/models/environment_model.freezed.dart @@ -24,6 +24,7 @@ mixin _$EnvironmentModel { String get name => throw _privateConstructorUsedError; List get values => throw _privateConstructorUsedError; + String? get color => throw _privateConstructorUsedError; /// Serializes this EnvironmentModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -41,7 +42,11 @@ abstract class $EnvironmentModelCopyWith<$Res> { EnvironmentModel value, $Res Function(EnvironmentModel) then) = _$EnvironmentModelCopyWithImpl<$Res, EnvironmentModel>; @useResult - $Res call({String id, String name, List values}); + $Res call( + {String id, + String name, + List values, + String? color}); } /// @nodoc @@ -62,6 +67,7 @@ class _$EnvironmentModelCopyWithImpl<$Res, $Val extends EnvironmentModel> Object? id = null, Object? name = null, Object? values = null, + Object? color = freezed, }) { return _then(_value.copyWith( id: null == id @@ -76,6 +82,10 @@ class _$EnvironmentModelCopyWithImpl<$Res, $Val extends EnvironmentModel> ? _value.values : values // ignore: cast_nullable_to_non_nullable as List, + color: freezed == color + ? _value.color + : color // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } } @@ -88,7 +98,11 @@ abstract class _$$EnvironmentModelImplCopyWith<$Res> __$$EnvironmentModelImplCopyWithImpl<$Res>; @override @useResult - $Res call({String id, String name, List values}); + $Res call( + {String id, + String name, + List values, + String? color}); } /// @nodoc @@ -107,6 +121,7 @@ class __$$EnvironmentModelImplCopyWithImpl<$Res> Object? id = null, Object? name = null, Object? values = null, + Object? color = freezed, }) { return _then(_$EnvironmentModelImpl( id: null == id @@ -121,6 +136,10 @@ class __$$EnvironmentModelImplCopyWithImpl<$Res> ? _value._values : values // ignore: cast_nullable_to_non_nullable as List, + color: freezed == color + ? _value.color + : color // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -132,7 +151,8 @@ class _$EnvironmentModelImpl implements _EnvironmentModel { const _$EnvironmentModelImpl( {required this.id, this.name = "", - final List values = const []}) + final List values = const [], + this.color}) : _values = values; factory _$EnvironmentModelImpl.fromJson(Map json) => @@ -152,9 +172,12 @@ class _$EnvironmentModelImpl implements _EnvironmentModel { return EqualUnmodifiableListView(_values); } + @override + final String? color; + @override String toString() { - return 'EnvironmentModel(id: $id, name: $name, values: $values)'; + return 'EnvironmentModel(id: $id, name: $name, values: $values, color: $color)'; } @override @@ -164,13 +187,14 @@ class _$EnvironmentModelImpl implements _EnvironmentModel { other is _$EnvironmentModelImpl && (identical(other.id, id) || other.id == id) && (identical(other.name, name) || other.name == name) && - const DeepCollectionEquality().equals(other._values, _values)); + const DeepCollectionEquality().equals(other._values, _values) && + (identical(other.color, color) || other.color == color)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, id, name, const DeepCollectionEquality().hash(_values)); + int get hashCode => Object.hash(runtimeType, id, name, + const DeepCollectionEquality().hash(_values), color); /// Create a copy of EnvironmentModel /// with the given fields replaced by the non-null parameter values. @@ -193,7 +217,8 @@ abstract class _EnvironmentModel implements EnvironmentModel { const factory _EnvironmentModel( {required final String id, final String name, - final List values}) = _$EnvironmentModelImpl; + final List values, + final String? color}) = _$EnvironmentModelImpl; factory _EnvironmentModel.fromJson(Map json) = _$EnvironmentModelImpl.fromJson; @@ -204,6 +229,8 @@ abstract class _EnvironmentModel implements EnvironmentModel { String get name; @override List get values; + @override + String? get color; /// Create a copy of EnvironmentModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/apidash_core/lib/models/environment_model.g.dart b/packages/apidash_core/lib/models/environment_model.g.dart index dd6b98e91..02a77784a 100644 --- a/packages/apidash_core/lib/models/environment_model.g.dart +++ b/packages/apidash_core/lib/models/environment_model.g.dart @@ -15,6 +15,7 @@ _$EnvironmentModelImpl _$$EnvironmentModelImplFromJson(Map json) => Map.from(e as Map))) .toList() ?? const [], + color: json['color'] as String?, ); Map _$$EnvironmentModelImplToJson( @@ -23,6 +24,7 @@ Map _$$EnvironmentModelImplToJson( 'id': instance.id, 'name': instance.name, 'values': instance.values.map((e) => e.toJson()).toList(), + 'color': instance.color, }; _$EnvironmentVariableModelImpl _$$EnvironmentVariableModelImplFromJson( diff --git a/pubspec.lock b/pubspec.lock index 662f03fc6..683b3296a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -570,6 +570,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_code_editor: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index bcb38b8e4..9c38b8164 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: file_selector: ^1.0.3 flex_color_scheme: ^8.2.0 flutter_code_editor: ^0.3.3 + flutter_colorpicker: ^1.1.0 flutter_highlight: ^0.7.0 flutter_highlighter: ^0.1.0 flutter_hooks: ^0.21.2