feat: environment autocomplete fields

This commit is contained in:
DenserMeerkat
2024-06-23 02:29:31 +05:30
parent 6db2968557
commit 7852fe98e5
15 changed files with 760 additions and 42 deletions

View File

@ -1,6 +1,7 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart' hide WindowCaption;
import 'widgets/widgets.dart' show WindowCaption;
@ -106,7 +107,8 @@ class DashApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isDarkMode =
ref.watch(settingsProvider.select((value) => value.isDark));
return MaterialApp(
return Portal(
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: kFontFamily,
@ -142,6 +144,7 @@ class DashApp extends ConsumerWidget {
),
],
),
),
);
}
}

View File

@ -66,6 +66,7 @@ const kFormDataButtonLabelTextStyle = TextStyle(
fontWeight: FontWeight.w600,
);
const kBorderRadius4 = BorderRadius.all(Radius.circular(4));
const kBorderRadius8 = BorderRadius.all(Radius.circular(8));
final kBorderRadius10 = BorderRadius.circular(10);
const kBorderRadius12 = BorderRadius.all(Radius.circular(12));
@ -286,6 +287,8 @@ enum FormDataType { text, file }
enum EnvironmentVariableType { variable, secret }
final kEnvVarRegEx = RegExp(r'{{(.*?)}}');
const kSupportedUriSchemes = ["https", "http"];
const kDefaultUriScheme = "https";
const kMethodsWithBody = [

View File

@ -42,3 +42,27 @@ const kEnvironmentVariableEmptyModel =
EnvironmentVariableModel(key: "", value: "");
const kEnvironmentSecretEmptyModel = EnvironmentVariableModel(
key: "", value: "", type: EnvironmentVariableType.secret);
class EnvironmentVariableSuggestion {
final String environmentId;
final EnvironmentVariableModel variable;
final bool isUnknown;
const EnvironmentVariableSuggestion({
required this.environmentId,
required this.variable,
this.isUnknown = false,
});
EnvironmentVariableSuggestion copyWith({
String? environmentId,
EnvironmentVariableModel? variable,
bool? isUnknown,
}) {
return EnvironmentVariableSuggestion(
environmentId: environmentId ?? this.environmentId,
variable: variable ?? this.variable,
isUnknown: isUnknown ?? this.isUnknown,
);
}
}

View File

@ -6,6 +6,7 @@ final mobileScaffoldKeyStateProvider = StateProvider<GlobalKey<ScaffoldState>>(
final leftDrawerStateProvider = StateProvider<bool>((ref) => false);
final navRailIndexStateProvider = StateProvider<int>((ref) => 0);
final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
final environmentFieldEditStateProvider = StateProvider<String?>((ref) => null);
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
final saveDataStateProvider = StateProvider<bool>((ref) => false);
final clearDataStateProvider = StateProvider<bool>((ref) => false);

View File

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/models/environment_model.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/envvar_utils.dart';
import 'package:apidash/widgets/widgets.dart';
class EnvironmentAutocompleteField extends HookConsumerWidget {
const EnvironmentAutocompleteField({
super.key,
required this.keyId,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
});
final String keyId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
@override
Widget build(BuildContext context, WidgetRef ref) {
final mentionValue = ref.watch(environmentFieldEditStateProvider);
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
final initialMentions =
getMentions(initialValue, envMap, activeEnvironmentId);
final suggestions = getEnvironmentVariableSuggestions(
mentionValue, envMap, activeEnvironmentId);
return EnvironmentAutocompleteFieldBase(
key: Key(keyId),
mentionValue: mentionValue,
onMentionValueChanged: (value) {
ref.read(environmentFieldEditStateProvider.notifier).state = value;
},
initialValue: initialValue,
initialMentions: initialMentions,
suggestions: suggestions,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
style: style,
decoration: decoration,
);
}
}
class EnvironmentVariableSpan extends HookConsumerWidget {
const EnvironmentVariableSpan({
super.key,
required this.suggestion,
});
final EnvironmentVariableSuggestion suggestion;
@override
Widget build(BuildContext context, WidgetRef ref) {
final environments = ref.watch(environmentsStateNotifierProvider);
final envMap = ref.watch(availableEnvironmentVariablesStateProvider);
final activeEnvironmentId = ref.watch(activeEnvironmentIdStateProvider);
final currentSuggestion =
getCurrentVariableStatus(suggestion, envMap, activeEnvironmentId);
final showPopover = useState(false);
final isMissingVariable = currentSuggestion.isUnknown;
final String scope = isMissingVariable
? 'unknown'
: getEnvironmentTitle(
environments?[currentSuggestion.environmentId]?.name);
final colorScheme = Theme.of(context).colorScheme;
var text = Text(
'{{${currentSuggestion.variable.key}}}',
style: TextStyle(
color: isMissingVariable ? colorScheme.error : colorScheme.primary,
fontWeight: FontWeight.w600),
);
return PortalTarget(
visible: showPopover.value,
portalFollower: MouseRegion(
onEnter: (_) {
showPopover.value = true;
},
onExit: (_) {
showPopover.value = false;
},
child:
EnvironmentPopoverCard(suggestion: currentSuggestion, scope: scope),
),
anchor: const Aligned(
follower: Alignment.bottomCenter,
target: Alignment.topCenter,
backup: Aligned(
follower: Alignment.topCenter,
target: Alignment.bottomCenter,
),
),
child: kIsMobile
? TapRegion(
onTapInside: (_) {
showPopover.value = true;
},
onTapOutside: (_) {
showPopover.value = false;
},
child: text,
)
: MouseRegion(
onEnter: (_) {
showPopover.value = true;
},
onExit: (_) {
showPopover.value = false;
},
child: text,
),
);
}
}
class EnvironmentPopoverCard extends StatelessWidget {
const EnvironmentPopoverCard({
super.key,
required this.suggestion,
required this.scope,
});
final EnvironmentVariableSuggestion suggestion;
final String scope;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.card,
borderRadius: kBorderRadius8,
elevation: 8,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200),
child: Ink(
padding: kP8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
EnvironmentIndicator(suggestion: suggestion),
kHSpacer10,
Text(suggestion.variable.key),
],
),
kVSpacer5,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'VALUE',
style: Theme.of(context).textTheme.labelSmall,
),
kHSpacer10,
Text(suggestion.variable.value),
],
),
kVSpacer5,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'SCOPE',
style: Theme.of(context).textTheme.labelSmall,
),
kHSpacer10,
Text(scope),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'environment_autocomplete.dart';
class EnvURLField extends StatelessWidget {
const EnvURLField({
super.key,
required this.selectedId,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
});
final String selectedId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
@override
Widget build(BuildContext context) {
return EnvironmentAutocompleteField(
keyId: "url-$selectedId",
initialValue: initialValue,
style: kCodeStyle,
decoration: InputDecoration(
hintText: kHintTextUrlCard,
hintStyle: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.outline.withOpacity(
kHintOpacity,
),
),
border: InputBorder.none,
),
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
);
}
}
class EnvCellField extends StatelessWidget {
const EnvCellField({
super.key,
required this.keyId,
this.initialValue,
this.hintText,
this.onChanged,
this.colorScheme,
});
final String keyId;
final String? initialValue;
final String? hintText;
final void Function(String)? onChanged;
final ColorScheme? colorScheme;
@override
Widget build(BuildContext context) {
var clrScheme = colorScheme ?? Theme.of(context).colorScheme;
return EnvironmentAutocompleteField(
keyId: keyId,
initialValue: initialValue,
style: kCodeStyle.copyWith(
color: clrScheme.onSurface,
),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(
color: clrScheme.outline.withOpacity(
kHintOpacity,
),
),
hintText: hintText,
contentPadding: const EdgeInsets.only(bottom: 12),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: clrScheme.primary.withOpacity(
kHintOpacity,
),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: clrScheme.surfaceVariant,
),
),
),
onChanged: onChanged,
);
}
}

View File

@ -86,6 +86,9 @@ class EnvironmentDropdown extends ConsumerWidget {
onChanged: (value) {
ref.read(activeEnvironmentIdStateProvider.notifier).state =
value?.id;
ref
.read(settingsProvider.notifier)
.update(activeEnvironmentId: value?.id);
ref.read(hasUnsavedChangesProvider.notifier).state = true;
},
));

View File

@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/screens/common/environment_textfields.dart';
class EditRequestHeaders extends ConsumerStatefulWidget {
const EditRequestHeaders({super.key});
@ -129,7 +130,7 @@ class EditRequestHeadersState extends ConsumerState<EditRequestHeaders> {
),
),
DataCell(
CellField(
EnvCellField(
keyId: "$selectedId-$index-headers-v-$seed",
initialValue: headerRows[index].value,
hintText: kHintAddValue,

View File

@ -1,11 +1,12 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:apidash/screens/common/environment_textfields.dart';
class EditRequestURLParams extends ConsumerStatefulWidget {
const EditRequestURLParams({super.key});
@ -103,7 +104,7 @@ class EditRequestURLParamsState extends ConsumerState<EditRequestURLParams> {
),
),
DataCell(
CellField(
EnvCellField(
keyId: "$selectedId-$index-params-k-$seed",
initialValue: paramRows[index].name,
hintText: kHintAddURLParam,
@ -129,7 +130,7 @@ class EditRequestURLParamsState extends ConsumerState<EditRequestURLParams> {
),
),
DataCell(
CellField(
EnvCellField(
keyId: "$selectedId-$index-params-v-$seed",
initialValue: paramRows[index].value,
hintText: kHintAddValue,

View File

@ -4,6 +4,7 @@ import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/extensions/extensions.dart';
import '../../common/environment_textfields.dart';
class EditorPaneRequestURLCard extends StatelessWidget {
const EditorPaneRequestURLCard({super.key});
@ -82,7 +83,7 @@ class URLTextField extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
return URLField(
return EnvURLField(
selectedId: selectedId!,
initialValue: ref
.read(collectionStateNotifierProvider.notifier)

View File

@ -1,5 +1,8 @@
import 'package:apidash/consts.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/screens/common/environment_autocomplete.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
String getEnvironmentTitle(String? name) {
if (name == null || name.trim() == "") {
@ -53,8 +56,7 @@ String? substituteVariables(
combinedMap[variable.key] = variable.value;
}
final regex = RegExp(r'{{(.*?)}}');
String result = input.replaceAllMapped(regex, (match) {
String result = input.replaceAllMapped(kEnvVarRegEx, (match) {
final key = match.group(1)?.trim() ?? '';
return combinedMap[key] ?? '';
});
@ -74,14 +76,124 @@ HttpRequestModel substituteHttpRequestModel(
)!,
headers: httpRequestModel.headers?.map((header) {
return header.copyWith(
name:
substituteVariables(header.name, envMap, activeEnvironmentId) ?? "",
value: substituteVariables(header.value, envMap, activeEnvironmentId),
);
}).toList(),
params: httpRequestModel.params?.map((param) {
return param.copyWith(
name:
substituteVariables(param.name, envMap, activeEnvironmentId) ?? "",
value: substituteVariables(param.value, envMap, activeEnvironmentId),
);
}).toList(),
);
return newRequestModel;
}
List<(String, Object?, Widget?)> getMentions(
String? text,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (text == null) {
return [];
}
final mentions = <(String, Object?, Widget?)>[];
final matches = kEnvVarRegEx.allMatches(text);
for (final match in matches) {
final variableName = match.group(1);
EnvironmentVariableModel? variable;
String? environmentId;
for (final entry in envMap.entries) {
variable = entry.value.firstWhereOrNull((v) => v.key == variableName);
if (variable != null) {
environmentId = entry.key;
break;
}
}
final suggestion = EnvironmentVariableSuggestion(
environmentId: variable == null ? "unknown" : environmentId!,
variable: variable ??
kEnvironmentVariableEmptyModel.copyWith(
key: variableName ?? "",
),
isUnknown: variable == null);
mentions.add((
'{{${variable?.key ?? variableName}}}',
suggestion,
EnvironmentVariableSpan(suggestion: suggestion)
));
}
return mentions;
}
EnvironmentVariableSuggestion getCurrentVariableStatus(
EnvironmentVariableSuggestion currentSuggestion,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (activeEnvironmentId != null) {
final variable = envMap[activeEnvironmentId]!
.firstWhereOrNull((v) => v.key == currentSuggestion.variable.key);
if (variable != null) {
return currentSuggestion.copyWith(
environmentId: activeEnvironmentId,
variable: variable,
isUnknown: false,
);
}
}
final globalVariable = envMap[kGlobalEnvironmentId]
?.firstWhereOrNull((v) => v.key == currentSuggestion.variable.key);
if (globalVariable != null) {
return currentSuggestion.copyWith(
environmentId: kGlobalEnvironmentId,
variable: globalVariable,
isUnknown: false,
);
}
return currentSuggestion.copyWith(
isUnknown: true,
variable: currentSuggestion.variable.copyWith(value: "unknown"));
}
List<EnvironmentVariableSuggestion>? getEnvironmentVariableSuggestions(
String? query,
Map<String, List<EnvironmentVariableModel>> envMap,
String? activeEnvironmentId) {
if (query == null || kEnvVarRegEx.hasMatch(query)) return null;
query = query.substring(1);
final suggestions = <EnvironmentVariableSuggestion>[];
final Set<String> addedVariableKeys = {};
if (activeEnvironmentId != null && envMap[activeEnvironmentId] != null) {
for (final variable in envMap[activeEnvironmentId]!) {
if ((query.isEmpty || variable.key.contains(query)) &&
!addedVariableKeys.contains(variable.key)) {
suggestions.add(EnvironmentVariableSuggestion(
environmentId: activeEnvironmentId, variable: variable));
addedVariableKeys.add(variable.key);
}
}
}
envMap[kGlobalEnvironmentId]?.forEach((variable) {
if ((query!.isEmpty || variable.key.contains(query)) &&
!addedVariableKeys.contains(variable.key)) {
suggestions.add(EnvironmentVariableSuggestion(
environmentId: kGlobalEnvironmentId, variable: variable));
addedVariableKeys.add(variable.key);
}
});
return suggestions;
}

View File

@ -0,0 +1,261 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:mention_tag_text_field/mention_tag_text_field.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart';
import '../screens/common/environment_autocomplete.dart';
class EnvironmentAutocompleteFieldBase extends StatefulHookWidget {
const EnvironmentAutocompleteFieldBase({
super.key,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
this.initialMentions,
this.suggestions,
this.mentionValue,
required this.onMentionValueChanged,
});
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
final List<(String, Object?, Widget?)>? initialMentions;
final List<EnvironmentVariableSuggestion>? suggestions;
final String? mentionValue;
final void Function(String?) onMentionValueChanged;
@override
State<EnvironmentAutocompleteFieldBase> createState() =>
_EnvironmentAutocompleteFieldBaseState();
}
class _EnvironmentAutocompleteFieldBaseState
extends State<EnvironmentAutocompleteFieldBase> {
final MentionTagTextEditingController controller =
MentionTagTextEditingController();
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
controller.text = widget.initialValue ?? "";
}
@override
Widget build(BuildContext context) {
final isSuggestionsVisible = useState(false);
return PortalTarget(
visible: isSuggestionsVisible.value && focusNode.hasFocus,
portalFollower: EnvironmentSuggestionsMenu(
mentionController: controller,
suggestions: widget.suggestions,
onSelect: (suggestion) {
controller.addMention(
label: '{${suggestion.variable.key}}}',
data: suggestion,
stylingWidget: EnvironmentVariableSpan(suggestion: suggestion));
widget.onChanged?.call(controller.text);
widget.onMentionValueChanged.call(null);
isSuggestionsVisible.value = false;
var mentionsCharacters =
controller.mentions.fold<int>(0, (previousValue, element) {
return previousValue + element.variable.key.length + 4 as int;
});
controller.selection = TextSelection.collapsed(
offset: controller.text.length +
controller.mentions.length -
mentionsCharacters,
);
},
),
anchor: const Aligned(
follower: Alignment.topLeft,
target: Alignment.bottomLeft,
backup: Aligned(
follower: Alignment.bottomLeft,
target: Alignment.topLeft,
),
),
child: EnvironmentMentionField(
focusNode: focusNode,
controller: controller,
initialMentions: widget.initialMentions ?? [],
mentionValue: widget.mentionValue,
onMentionValueChanged: widget.onMentionValueChanged,
isSuggestionsVisible: isSuggestionsVisible,
onChanged: widget.onChanged,
onFieldSubmitted: widget.onFieldSubmitted,
style: widget.style,
decoration: widget.decoration,
),
);
}
}
class EnvironmentMentionField extends StatelessWidget {
const EnvironmentMentionField({
super.key,
required this.focusNode,
required this.controller,
required this.initialMentions,
required this.mentionValue,
required this.onMentionValueChanged,
required this.isSuggestionsVisible,
this.onChanged,
this.onFieldSubmitted,
this.style,
this.decoration,
});
final FocusNode focusNode;
final MentionTagTextEditingController controller;
final List<(String, Object?, Widget?)> initialMentions;
final String? mentionValue;
final void Function(String?) onMentionValueChanged;
final ValueNotifier<bool> isSuggestionsVisible;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
final TextStyle? style;
final InputDecoration? decoration;
void onMention(String? value) {
onMentionValueChanged.call(value);
if (value != null) {
isSuggestionsVisible.value = true;
} else {
isSuggestionsVisible.value = false;
}
}
@override
Widget build(BuildContext context) {
return MentionTagTextFormField(
focusNode: focusNode,
onTap: () {
focusNode.requestFocus();
},
onTapOutside: (event) {
focusNode.unfocus();
isSuggestionsVisible.value = false;
},
controller: controller,
style: style,
initialMentions: initialMentions,
onMention: onMention,
onChanged: (value) {
onChanged?.call(controller.text);
},
onFieldSubmitted: (value) {
onFieldSubmitted?.call(controller.text);
isSuggestionsVisible.value = false;
},
decoration: decoration,
mentionTagDecoration: const MentionTagDecoration(
mentionStart: ['{'],
mentionBreak:
" ", // This is a workaround for the exception but adds a space after the mention
maxWords: 1,
allowDecrement: false,
allowEmbedding: true,
showMentionStartSymbol: false,
),
);
}
}
class EnvironmentSuggestionsMenu extends StatelessWidget {
const EnvironmentSuggestionsMenu({
super.key,
required this.mentionController,
required this.suggestions,
this.onSelect,
});
final MentionTagTextEditingController mentionController;
final List<EnvironmentVariableSuggestion>? suggestions;
final Function(EnvironmentVariableSuggestion)? onSelect;
@override
Widget build(BuildContext context) {
return suggestions == null || suggestions!.isEmpty
? const SizedBox.shrink()
: ClipRRect(
borderRadius: kBorderRadius8,
child: Material(
type: MaterialType.card,
elevation: 8,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: Ink(
width: 300,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: kBorderRadius8,
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: ListView.separated(
shrinkWrap: true,
itemCount: suggestions?.length ?? 0,
separatorBuilder: (context, index) => const Divider(
height: 2,
),
itemBuilder: (context, index) {
final suggestion = suggestions![index];
return ListTile(
dense: true,
leading: EnvironmentIndicator(suggestion: suggestion),
title: Text(suggestion.variable.key),
subtitle: Text(suggestion.variable.value),
onTap: () {
onSelect?.call(suggestions![index]);
},
);
},
),
),
),
),
);
}
}
class EnvironmentIndicator extends StatelessWidget {
const EnvironmentIndicator({super.key, required this.suggestion});
final EnvironmentVariableSuggestion suggestion;
@override
Widget build(BuildContext context) {
final isUnknown = suggestion.isUnknown;
final isGlobal = suggestion.environmentId == kGlobalEnvironmentId;
return Container(
padding: kP4,
decoration: BoxDecoration(
color: isUnknown
? Theme.of(context).colorScheme.errorContainer
: isGlobal
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primaryContainer,
borderRadius: kBorderRadius4,
),
child: Icon(
isUnknown
? Icons.block
: isGlobal
? Icons.public
: Icons.computer,
size: 16,
),
);
}
}

View File

@ -6,6 +6,7 @@ export 'codegen_previewer.dart';
export 'dropdowns.dart';
export 'editor_json.dart';
export 'editor.dart';
export 'environment_field.dart';
export 'error_message.dart';
export 'headerfield.dart';
export 'intro_message.dart';

View File

@ -470,6 +470,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.23"
flutter_portal:
dependency: "direct main"
description:
name: flutter_portal
sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
flutter_riverpod:
dependency: "direct main"
description:
@ -809,6 +817,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.0"
mention_tag_text_field:
dependency: "direct main"
description:
name: mention_tag_text_field
sha256: b0e831f5fc8ca942a835916fd084e6f5054226715affc740c613cd320004b8a7
url: "https://pub.dev"
source: hosted
version: "0.0.4"
meta:
dependency: transitive
description:
@ -1520,5 +1536,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.3.0 <4.0.0"
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.19.2"

View File

@ -62,6 +62,8 @@ dependencies:
file_selector: ^1.0.3
hooks_riverpod: ^2.5.1
flutter_hooks: ^0.20.5
flutter_portal: ^1.1.4
mention_tag_text_field: ^0.0.4
dependency_overrides:
web: ^0.5.0