Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions lib/screens/common_widgets/env_trigger_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class EnvironmentTriggerField extends StatefulWidget {
this.optionsWidthFactor,
this.autocompleteNoTrigger,
this.readOnly = false,
this.obscureText = false
this.obscureText = false,
this.minLines,
this.maxLines,
}) : assert(
!(controller != null && initialValue != null),
'controller and initialValue cannot be simultaneously defined.',
Expand All @@ -36,6 +38,8 @@ class EnvironmentTriggerField extends StatefulWidget {
final AutocompleteNoTrigger? autocompleteNoTrigger;
final bool readOnly;
final bool obscureText;
final int? minLines;
final int? maxLines;

@override
State<EnvironmentTriggerField> createState() =>
Expand Down Expand Up @@ -130,8 +134,9 @@ class EnvironmentTriggerFieldState extends State<EnvironmentTriggerField> {
_focusNode.unfocus();
},
readOnly: widget.readOnly,
obscureText: widget.obscureText

obscureText: widget.obscureText,
minLines: widget.minLines,
maxLines: widget.maxLines,
);
},
);
Expand Down
1 change: 1 addition & 0 deletions lib/screens/common_widgets/envfield_url.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class EnvURLField extends StatelessWidget {
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
optionsWidthFactor: 1,
maxLines: 1,
);
}
}
188 changes: 188 additions & 0 deletions lib/screens/home_page/editor_pane/expanded_url_editor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/screens/common_widgets/env_trigger_field.dart';

class ExpandedURLEditor extends StatefulWidget {
const ExpandedURLEditor({
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
State<ExpandedURLEditor> createState() => _ExpandedURLEditorState();
}

class _ExpandedURLEditorState extends State<ExpandedURLEditor> {
late TextEditingController _controller;
late FocusNode _focusNode;

@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialValue);
_focusNode = FocusNode();
_focusNode.requestFocus();
}

@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}

void _moveCursorToNextSeparator() {
final text = _controller.text;
final currentPos = _controller.selection.baseOffset;
if (currentPos < 0 || currentPos >= text.length) return;

final separators = RegExp(r'[:/.,?&=-_]');

int nextPos = -1;
for (int i = currentPos + 1; i < text.length; i++) {
if (separators.hasMatch(text[i])) {
nextPos = i;
break;
}
}

if (nextPos != -1) {
_controller.selection = TextSelection.fromPosition(TextPosition(offset: nextPos));
} else {
_controller.selection = TextSelection.fromPosition(TextPosition(offset: text.length));
}
_focusNode.requestFocus();
}

void _moveCursorToPreviousSeparator() {
final text = _controller.text;
final currentPos = _controller.selection.baseOffset;
if (currentPos <= 0) return;

final separators = RegExp(r'[:/.,?&=-_]');

int prevPos = -1;
for (int i = currentPos - 1; i >= 0; i--) {
if (separators.hasMatch(text[i])) {
prevPos = i;
break;
}
}

if (prevPos != -1) {
_controller.selection = TextSelection.fromPosition(TextPosition(offset: prevPos));
} else {
_controller.selection = TextSelection.fromPosition(const TextPosition(offset: 0));
}
_focusNode.requestFocus();
}

@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
insetPadding: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min, // Important for Dialog
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Edit URL',
style: Theme.of(context).textTheme.titleLarge,
),
TextButton(
onPressed: () {
widget.onFieldSubmitted?.call(_controller.text);
Navigator.of(context).pop();
},
child: const Text('Done'),
),
],
),
const SizedBox(height: 16),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.6,
),
child: SingleChildScrollView(
child: EnvironmentTriggerField(
keyId: "expanded-url-${widget.selectedId}",
controller: _controller,
focusNode: _focusNode,
style: kCodeStyle.copyWith(fontSize: 16),
decoration: InputDecoration(
hintText: kHintTextUrlCard,
hintStyle: kCodeStyle.copyWith(
color: Theme.of(context).colorScheme.outlineVariant,
fontSize: 16,
),
border: InputBorder.none,
),
onChanged: widget.onChanged,
onFieldSubmitted: (val) {
widget.onFieldSubmitted?.call(val);
Navigator.of(context).pop();
},
optionsWidthFactor: 1,
maxLines: null, // Allow unlimited lines (wrapping)
minLines: 3, // Minimum height
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton.filledTonal(
onPressed: _moveCursorToPreviousSeparator,
icon: const Icon(Icons.keyboard_arrow_left),
tooltip: 'Previous Separator',
),
IconButton.filledTonal(
onPressed: _moveCursorToNextSeparator,
icon: const Icon(Icons.keyboard_arrow_right),
tooltip: 'Next Separator',
),
IconButton.filledTonal(
onPressed: () {
Clipboard.setData(ClipboardData(text: _controller.text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
icon: const Icon(Icons.copy),
tooltip: 'Copy',
),
IconButton.filledTonal(
onPressed: () {
_controller.clear();
widget.onChanged?.call('');
},
icon: const Icon(Icons.clear),
tooltip: 'Clear',
),
],
),
],
),
),
);
}
}
74 changes: 56 additions & 18 deletions lib/screens/home_page/editor_pane/url_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import '../../common_widgets/common_widgets.dart';
import 'expanded_url_editor.dart';

class EditorPaneRequestURLCard extends ConsumerWidget {
const EditorPaneRequestURLCard({super.key});
Expand Down Expand Up @@ -94,13 +95,20 @@ class DropdownButtonHTTPMethod extends ConsumerWidget {
}
}

class URLTextField extends ConsumerWidget {
class URLTextField extends ConsumerStatefulWidget {
const URLTextField({
super.key,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<URLTextField> createState() => _URLTextFieldState();
}

class _URLTextFieldState extends ConsumerState<URLTextField> {
bool _isNavigating = false;

@override
Widget build(BuildContext context) {
final selectedId = ref.watch(selectedIdStateProvider);
ref.watch(selectedRequestModelProvider
.select((value) => value?.aiRequestModel?.url));
Expand All @@ -109,24 +117,54 @@ class URLTextField extends ConsumerWidget {
final requestModel = ref
.read(collectionStateNotifierProvider.notifier)
.getRequestModel(selectedId!)!;
return EnvURLField(
selectedId: selectedId,
initialValue: switch (requestModel.apiType) {
APIType.ai => requestModel.aiRequestModel?.url,
_ => requestModel.httpRequestModel?.url,
},
onChanged: (value) {
if (requestModel.apiType == APIType.ai) {
ref.read(collectionStateNotifierProvider.notifier).update(
aiRequestModel:
requestModel.aiRequestModel?.copyWith(url: value));
} else {
ref.read(collectionStateNotifierProvider.notifier).update(url: value);

final initialValue = switch (requestModel.apiType) {
APIType.ai => requestModel.aiRequestModel?.url,
_ => requestModel.httpRequestModel?.url,
};

void updateUrl(String value) {
if (requestModel.apiType == APIType.ai) {
ref.read(collectionStateNotifierProvider.notifier).update(
aiRequestModel:
requestModel.aiRequestModel?.copyWith(url: value));
} else {
ref.read(collectionStateNotifierProvider.notifier).update(url: value);
}
}

return GestureDetector(
onScaleUpdate: (details) async {
if (details.scale > 1.5 && !_isNavigating) {
_isNavigating = true;
// HapticFeedback.mediumImpact(); // Optional: Add haptic feedback
await showDialog(
context: context,
builder: (context) => ExpandedURLEditor(
selectedId: selectedId,
initialValue: initialValue,
onChanged: updateUrl,
onFieldSubmitted: (value) {
updateUrl(value);
ref.read(collectionStateNotifierProvider.notifier).sendRequest();
},
),
);
if (mounted) {
setState(() {
_isNavigating = false;
});
}
}
},
onFieldSubmitted: (value) {
ref.read(collectionStateNotifierProvider.notifier).sendRequest();
},
child: EnvURLField(
selectedId: selectedId,
initialValue: initialValue,
onChanged: updateUrl,
onFieldSubmitted: (value) {
ref.read(collectionStateNotifierProvider.notifier).sendRequest();
},
),
);
}
}
Expand Down