diff --git a/lib/components/collection_pane.dart b/lib/components/collection_pane.dart index 9583e683..88fd19b7 100644 --- a/lib/components/collection_pane.dart +++ b/lib/components/collection_pane.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/models.dart'; +import '../providers/providers.dart'; +import '../consts.dart'; -class CollectionPane extends StatefulWidget { +class CollectionPane extends ConsumerStatefulWidget { const CollectionPane({ Key? key, }) : super(key: key); @override - State createState() => _CollectionPaneState(); + ConsumerState createState() => _CollectionPaneState(); } -class _CollectionPaneState extends State { - int len = 0; - +class _CollectionPaneState extends ConsumerState { @override void initState() { super.initState(); @@ -29,9 +31,11 @@ class _CollectionPaneState extends State { children: [ ElevatedButton( onPressed: () { - setState(() { - len += 1; - }); + String newId = + ref.read(collectionStateNotifierProvider.notifier).add(); + ref + .read(activeItemIdStateProvider.notifier) + .update((state) => newId); }, child: const Text('+ New'), ), @@ -40,9 +44,9 @@ class _CollectionPaneState extends State { const SizedBox( height: 8, ), - Expanded( + const Expanded( child: SingleChildScrollView( - child: RequestList(l: len), + child: RequestList(), ), ), ], @@ -51,21 +55,16 @@ class _CollectionPaneState extends State { } } -class RequestList extends StatefulWidget { +class RequestList extends ConsumerStatefulWidget { const RequestList({ Key? key, - required this.l, }) : super(key: key); - final int l; - @override - State createState() => _RequestListState(); + ConsumerState createState() => _RequestListState(); } -class _RequestListState extends State { - List requestItems = []; - +class _RequestListState extends ConsumerState { @override void initState() { super.initState(); @@ -73,9 +72,7 @@ class _RequestListState extends State { @override Widget build(BuildContext context) { - if (requestItems.length != widget.l) { - requestItems = List.generate(widget.l, (index) => "request${index + 1}"); - } + final requestItems = ref.watch(collectionStateNotifierProvider); return ReorderableListView.builder( buildDefaultDragHandles: false, shrinkWrap: true, @@ -85,19 +82,17 @@ class _RequestListState extends State { newIndex -= 1; } if (oldIndex != newIndex) { - var t = requestItems[oldIndex]; - requestItems[oldIndex] = requestItems[newIndex]; - requestItems[newIndex] = t; - setState(() { - requestItems = [...requestItems]; - }); + ref + .read(collectionStateNotifierProvider.notifier) + .reorder(oldIndex, newIndex); } }, itemBuilder: (context, index) { return ReorderableDragStartListener( - key: Key(requestItems[index]), + key: Key(requestItems[index].id), index: index, - child: RequestItem(id: requestItems[index]), + child: RequestItem( + id: requestItems[index].id, requestModel: requestItems[index]), ); }, ); @@ -106,19 +101,21 @@ class _RequestListState extends State { enum RequestItemMenuOption { delete, duplicate } -class RequestItem extends StatefulWidget { +class RequestItem extends ConsumerStatefulWidget { const RequestItem({ required this.id, + required this.requestModel, Key? key, }) : super(key: key); final String id; + final RequestModel requestModel; @override - State createState() => _RequestItemState(); + ConsumerState createState() => _RequestItemState(); } -class _RequestItemState extends State { +class _RequestItemState extends ConsumerState { late Color _color; @override @@ -129,17 +126,23 @@ class _RequestItemState extends State { @override Widget build(BuildContext context) { + final activeRequest = ref.watch(activeItemIdStateProvider); + bool isActiveId = activeRequest == widget.id; return Material( borderRadius: BorderRadius.circular(10.0), - elevation: 1, - color: Colors.grey.shade50, + elevation: isActiveId ? 2 : 0, + color: isActiveId ? Colors.grey.shade300 : _color, child: InkWell( borderRadius: BorderRadius.circular(10.0), - onTap: () {}, + onTap: () { + ref + .read(activeItemIdStateProvider.notifier) + .update((state) => widget.id); + }, child: Padding( padding: EdgeInsets.only( left: 10, - right: 0, + right: isActiveId ? 0 : 20, top: 5, bottom: 5, ), @@ -147,30 +150,50 @@ class _RequestItemState extends State { height: 22, child: Row( children: [ - MethodBox(), + MethodBox(method: widget.requestModel.method), const SizedBox( width: 5, ), Expanded( child: Text( - widget.id, + widget.requestModel.id, + softWrap: false, + overflow: TextOverflow.fade, ), ), - PopupMenuButton( - padding: EdgeInsets.zero, - splashRadius: 14, - iconSize: 14, - itemBuilder: (BuildContext context) => - >[ - const PopupMenuItem( - value: RequestItemMenuOption.delete, - child: Text('Delete'), - ), - const PopupMenuItem( - value: RequestItemMenuOption.duplicate, - child: Text('Duplicate'), - ), - ], + Visibility( + visible: isActiveId, + child: PopupMenuButton( + padding: EdgeInsets.zero, + splashRadius: 14, + iconSize: 14, + onSelected: (RequestItemMenuOption item) { + if (item == RequestItemMenuOption.delete) { + ref + .read(activeItemIdStateProvider.notifier) + .update((state) => null); + ref + .read(collectionStateNotifierProvider.notifier) + .remove(widget.id); + } + if (item == RequestItemMenuOption.duplicate) { + ref + .read(collectionStateNotifierProvider.notifier) + .duplicate(widget.id); + } + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: RequestItemMenuOption.delete, + child: Text('Delete'), + ), + const PopupMenuItem( + value: RequestItemMenuOption.duplicate, + child: Text('Duplicate'), + ), + ], + ), ), ], ), @@ -182,11 +205,15 @@ class _RequestItemState extends State { } class MethodBox extends StatelessWidget { - const MethodBox({super.key}); + const MethodBox({super.key, required this.method}); + final HTTPVerb method; @override Widget build(BuildContext context) { - String text = "get".toUpperCase(); + String text = method.name.toUpperCase(); + if (method == HTTPVerb.delete) { + text = "DEL"; + } return SizedBox( width: 25, child: Text( diff --git a/lib/components/components.dart b/lib/components/components.dart index 9e68a335..2b85920e 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -1 +1,2 @@ +export 'editor_pane.dart'; export 'collection_pane.dart'; diff --git a/lib/components/editor_pane.dart b/lib/components/editor_pane.dart new file mode 100644 index 00000000..b7876640 --- /dev/null +++ b/lib/components/editor_pane.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../providers/providers.dart'; + +class RequestEditorPane extends ConsumerStatefulWidget { + const RequestEditorPane({ + Key? key, + }) : super(key: key); + + @override + ConsumerState createState() => _RequestEditorPaneState(); +} + +class _RequestEditorPaneState extends ConsumerState { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final activeId = ref.watch(activeItemIdStateProvider); + if (activeId == null) { + return Text("Select One"); + } else { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Container(), + ], + ), + ); + } + } +} diff --git a/lib/consts.dart b/lib/consts.dart new file mode 100644 index 00000000..b1f50b81 --- /dev/null +++ b/lib/consts.dart @@ -0,0 +1,3 @@ +enum HTTPVerb { get, head, post, put, patch, delete } + +const DEFAULT_METHOD = HTTPVerb.get; diff --git a/lib/main.dart b/lib/main.dart index 3618547b..b6158e6d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,20 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:desktop_window/desktop_window.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'screens/screens.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - Size size = await DesktopWindow.getWindowSize(); - print(size); - await DesktopWindow.setWindowSize(Size(1400, 800)); - await DesktopWindow.setMinWindowSize(Size(1200, 800)); - runApp(App()); + if (!kIsWeb) { + await DesktopWindow.setWindowSize(Size(1400, 800)); + await DesktopWindow.setMinWindowSize(Size(1200, 800)); + } + runApp( + const ProviderScope( + child: App(), + ), + ); } class App extends StatelessWidget { diff --git a/lib/models/models.dart b/lib/models/models.dart new file mode 100644 index 00000000..1c83191d --- /dev/null +++ b/lib/models/models.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import '../consts.dart'; + +@immutable +class RequestModel { + const RequestModel({ + required this.id, + this.method = DEFAULT_METHOD, + this.url = "", + }); + + final String id; + final HTTPVerb method; + final String url; + + RequestModel duplicate({ + required String id, + }) { + return RequestModel( + id: id, + method: method, + url: url, + ); + } + + RequestModel copyWith({ + String? id, + HTTPVerb? method, + String? url, + }) { + return RequestModel( + id: id ?? this.id, + method: method ?? this.method, + url: url ?? this.url, + ); + } + + @override + String toString() { + return [ + id, + method.name, + "URL: $url", + ].join("\n"); + } +} diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart new file mode 100644 index 00000000..c0af24a0 --- /dev/null +++ b/lib/providers/providers.dart @@ -0,0 +1,74 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uuid/uuid.dart'; +import '../models/models.dart'; +import '../consts.dart'; + +const _uuid = Uuid(); + +final activeItemIdStateProvider = StateProvider((ref) => null); + +final StateNotifierProvider> + collectionStateNotifierProvider = + StateNotifierProvider((ref) => CollectionStateNotifier()); + +class CollectionStateNotifier extends StateNotifier> { + CollectionStateNotifier() + : super([ + RequestModel( + id: _uuid.v1(), + ), + ]); + + int idxOfId(String id) => state.indexWhere((element) => element.id == id); + + RequestModel getRequestModel(String id) { + final idx = idxOfId(id); + return state[idx]; + } + + String add() { + final newRequestModel = RequestModel( + id: _uuid.v1(), + ); + state = [newRequestModel, ...state]; + return newRequestModel.id; + } + + void reorder(int oldIdx, int newIdx) { + final item = state.removeAt(oldIdx); + state.insert(newIdx, item); + } + + void remove(String id) { + state = [ + for (final model in state) + if (model.id != id) model, + ]; + } + + void duplicate(String id) { + final idx = idxOfId(id); + final newModel = state[idx].duplicate( + id: _uuid.v1(), + ); + state = [...state.sublist(0, idx + 1), newModel, ...state.sublist(idx + 1)]; + } + + void update( + String id, { + HTTPVerb? method, + String? url, + int? requestTabIndex, + dynamic requestBody, + int? responseStatus, + Map? responseHeaders, + String? responseBody, + }) { + final idx = idxOfId(id); + final newModel = state[idx].copyWith( + method: method, + url: url, + ); + state = [...state.sublist(0, idx), newModel, ...state.sublist(idx + 1)]; + } +} diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart new file mode 100644 index 00000000..d6af7bf9 --- /dev/null +++ b/lib/screens/home_page.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import '../components/components.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + HomePageState createState() => HomePageState(); +} + +class HomePageState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(size: 300, minimalSize: 200), + Area(minimalWeight: 0.7), + ], + ); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 4, + dividerPainter: DividerPainters.background( + color: Colors.grey.shade200, + highlightedColor: Colors.grey.shade400, + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + children: const [ + CollectionPane(), + RequestEditorPane(), + ], + ), + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart index 3bc4a44b..6a45182d 100644 --- a/lib/screens/screens.dart +++ b/lib/screens/screens.dart @@ -1,60 +1 @@ -import 'package:flutter/material.dart'; -import 'package:multi_split_view/multi_split_view.dart'; -import '../components/components.dart'; - -class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); - - @override - HomePageState createState() => HomePageState(); -} - -class HomePageState extends State { - final MultiSplitViewController _controller = MultiSplitViewController( - areas: [ - Area(size: 300, minimalSize: 200), - Area(minimalWeight: 0.7), - ], - ); - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - // appBar: AppBar(title: const Text('Multi Split View Example')), - body: Column( - children: [ - Expanded( - child: MultiSplitViewTheme( - data: MultiSplitViewThemeData( - dividerThickness: 4, - dividerPainter: DividerPainters.background( - color: Colors.grey.shade200, - highlightedColor: Colors.grey.shade400, - animationEnabled: false, - ), - ), - child: MultiSplitView( - controller: _controller, - children: [ - CollectionPane(), - Container(), - ], - ), - ), - ), - ], - ), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} +export "home_page.dart"; diff --git a/pubspec.lock b/pubspec.lock index 03027dbc..ad03a0c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" desktop_window: dependency: "direct main" description: @@ -70,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "40c0d7d03ccd24fa32ea08dcfc4df9bb92c4c26c9a91938945da3be10ed8ca83" + url: "https://pub.dev" + source: hosted + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -131,6 +147,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.2" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: c5aea6c3fed340707f013a023a37ab388bf45611a8a4f7e76b5e9007eb76cd25 + url: "https://pub.dev" + source: hosted + version: "2.3.0" sky_engine: dependency: transitive description: flutter @@ -152,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" + source: hosted + version: "0.7.2+1" stream_channel: dependency: transitive description: @@ -184,6 +216,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -194,4 +242,4 @@ packages: version: "2.1.4" sdks: dart: ">=2.19.2 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index b115b7a0..730b23a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,9 @@ dependencies: sdk: flutter multi_split_view: ^2.4.0 desktop_window: ^0.4.0 - + flutter_riverpod: ^2.1.3 + uuid: ^3.0.7 + dev_dependencies: flutter_test: sdk: flutter