diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 88e45ec2..49dfe609 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,16 +7,23 @@ assignees: '' --- -**Describe the bug/problem** +#### Describe the bug/problem _Please describe the bug/problem here_ -**Steps to Reproduce the bug/problem** +#### Steps to Reproduce the bug/problem _Please provide the steps to reproduce the behaviour. Screenshot(s)/image(s) are preferred to help explain it better_ -**Expected behavior** +#### Expected behavior _Description of what you expected to happen_ -**Device Info (The device where you encountered this issue):** +#### Device Info (The device where you encountered this issue) - OS: [e.g. Windows, MacOS] - Version: [e.g. Catalina 10.15.7, Monterey 12.3.1, Windows 11 22H2] - Browser (only if you encountered the issue while running the web app): [e.g. chrome, safari] + +#### Flutter Doctor +Please run the `flutter doctor -v` command and provide the details below: + +``` +<insert flutter doctor output here> +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7c1ec21d..9264284f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,11 +7,11 @@ assignees: '' --- -**Tell us about the task you want to perform and are unable to do so because the feature is not available** +#### Tell us about the task you want to perform and are unable to do so because the feature is not available _Your response here_ -**Describe the solution/feature you'd like us to add** +#### Describe the solution/feature you'd like us to add _Your response here_ -**Any other feedback you would like to provide regarding the site** +#### Any other feedback you would like to provide regarding the site _Your response here_ diff --git a/.github/ISSUE_TEMPLATE/feedback.md b/.github/ISSUE_TEMPLATE/feedback.md index 5c8d50a6..31df2527 100644 --- a/.github/ISSUE_TEMPLATE/feedback.md +++ b/.github/ISSUE_TEMPLATE/feedback.md @@ -8,5 +8,5 @@ assignees: '' --- -**Feedback** +#### Feedback _Please feel free to share anything. The stage is all yours._ diff --git a/.gitignore b/.gitignore index 4bdfd329..9a64d7c0 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ coverage/* installers/* .metadata .fvm/ + +# Testing Files & Folders +test-hive-storage diff --git a/ROADMAP.md b/ROADMAP.md index 8b4ce1ac..a38951b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ ## API Dash Roadmap -- [ ] Environment Variables (https://github.com/foss42/apidash/issues/25) +- [x] Environment Variables (https://github.com/foss42/apidash/issues/25) - [ ] WebSocket support (https://github.com/foss42/apidash/issues/15) - [ ] SSE support (https://github.com/foss42/apidash/issues/116) - [ ] MQTT support (https://github.com/foss42/apidash/issues/115) @@ -8,7 +8,7 @@ - [ ] gRPC support (https://github.com/foss42/apidash/issues/14) - [ ] API Testing Suite (https://github.com/foss42/apidash/discussions/96, https://github.com/foss42/apidash/issues/100) - [ ] API Workflow Builder (https://github.com/foss42/apidash/issues/120) -- [ ] Integration Testing (https://github.com/foss42/apidash/issues/119) +- [x] Integration Testing (https://github.com/foss42/apidash/issues/119) - [ ] Remaining Code Generators (https://github.com/foss42/apidash/discussions/80) - [ ] Embedded WebView in Response Previewer - [ ] Figuring out how to build for various Linux packaging formats (https://github.com/foss42/apidash/discussions/240) diff --git a/flutter_launcher_icons.yaml b/flutter_launcher_icons.yaml index 532dd379..21a66d08 100644 --- a/flutter_launcher_icons.yaml +++ b/flutter_launcher_icons.yaml @@ -1,14 +1,14 @@ -flutter_icons: +flutter_launcher_icons: image_path: "icons/logo.png" - android: false #"ic_launcher" + android: true ios: false image_path_ios: "icons/logo_ios.png" min_sdk_android: 21 web: - generate: true + generate: false windows: - generate: true - icon_size: 256 + generate: false + icon_size: 256 macos: - generate: true + generate: false image_path: "icons/logo_macos.png" diff --git a/flutter_native_splash.yaml b/flutter_native_splash.yaml new file mode 100644 index 00000000..c3b079ce --- /dev/null +++ b/flutter_native_splash.yaml @@ -0,0 +1,10 @@ +flutter_native_splash: + color: "#f8f9ff" + image: icons/logo.png + android_12: + color: "#f8f9ff" + image: icons/logo_android12.png + + android: true + ios: false + web: false diff --git a/integration_test/test_helper.dart b/integration_test/test_helper.dart index 70cafa9d..f05a7cf7 100644 --- a/integration_test/test_helper.dart +++ b/integration_test/test_helper.dart @@ -1,3 +1,6 @@ +import 'package:apidash/models/settings_model.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/services/services.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -34,17 +37,24 @@ class ApidashTestHelper { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - await app.initApp(); + await app.initApp(false); await app.initWindow(sz: size); return binding; } static Future<void> loadApp(WidgetTester tester) async { - await app.initApp(); + await app.initApp(false); await tester.pumpWidget( - const ProviderScope( - child: DashApp(), + ProviderScope( + overrides: [ + settingsProvider.overrideWith( + (ref) => ThemeStateNotifier( + settingsModel: const SettingsModel() + .copyWithPath(workspaceFolderPath: "test")), + ) + ], + child: const DashApp(), ), ); } @@ -123,6 +133,7 @@ void apidashWidgetTest( size: width != null ? Size(width, kMinWindowSize.height) : null); await ApidashTestHelper.loadApp(widgetTester); await test(widgetTester, ApidashTestHelper(widgetTester)); + await clearSharedPrefs(); }, semanticsEnabled: false, ); diff --git a/lib/app.dart b/lib/app.dart index 3d36968a..48c49a86 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,8 +4,9 @@ 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; +import 'widgets/widgets.dart' show WindowCaption, WorkspaceSelector; import 'providers/providers.dart'; +import 'services/services.dart'; import 'extensions/extensions.dart'; import 'screens/screens.dart'; import 'consts.dart'; @@ -107,29 +108,49 @@ class DashApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isDarkMode = ref.watch(settingsProvider.select((value) => value.isDark)); + final workspaceFolderPath = ref + .watch(settingsProvider.select((value) => value.workspaceFolderPath)); + final showWorkspaceSelector = kIsDesktop && (workspaceFolderPath == null); return Portal( child: MaterialApp( debugShowCheckedModeBanner: false, theme: kLightMaterialAppTheme, darkTheme: kDarkMaterialAppTheme, themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, - home: Stack( - children: [ - !kIsLinux && !kIsMobile - ? const App() - : context.isMediumWindow - ? const MobileDashboard() - : const Dashboard(), - if (kIsWindows) - SizedBox( - height: 29, - child: WindowCaption( - backgroundColor: Colors.transparent, - brightness: isDarkMode ? Brightness.dark : Brightness.light, - ), + home: showWorkspaceSelector + ? WorkspaceSelector( + onContinue: (val) async { + await initHiveBoxes(kIsDesktop, val); + ref + .read(settingsProvider.notifier) + .update(workspaceFolderPath: val); + }, + onCancel: () async { + try { + await windowManager.destroy(); + } catch (e) { + debugPrint(e.toString()); + } + }, + ) + : Stack( + children: [ + !kIsLinux && !kIsMobile + ? const App() + : context.isMediumWindow + ? const MobileDashboard() + : const Dashboard(), + if (kIsWindows) + SizedBox( + height: 29, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: + isDarkMode ? Brightness.dark : Brightness.light, + ), + ), + ], ), - ], - ), ), ); } diff --git a/lib/codegen/java/okhttp.dart b/lib/codegen/java/okhttp.dart index 12b2f90f..9045aa55 100644 --- a/lib/codegen/java/okhttp.dart +++ b/lib/codegen/java/okhttp.dart @@ -140,7 +140,7 @@ import okhttp3.MultipartBody;"""; var templateBody = jj.Template(kTemplateRequestBody); result += templateBody.render({ "contentType": contentType, - "body": kEncoder.convert(requestBody) + "body": kJsonEncoder.convert(requestBody) }); } } diff --git a/lib/codegen/js/axios.dart b/lib/codegen/js/axios.dart index faf97304..6314bdcd 100644 --- a/lib/codegen/js/axios.dart +++ b/lib/codegen/js/axios.dart @@ -79,7 +79,7 @@ axios(config) m[i["name"]] = i["value"]; } result += templateParams - .render({"params": padMultilineString(kEncoder.convert(m), 2)}); + .render({"params": padMultilineString(kJsonEncoder.convert(m), 2)}); } var headers = harJson["headers"]; @@ -92,8 +92,8 @@ axios(config) if (requestModel.hasFormData) { m[kHeaderContentType] = ContentType.formdata.header; } - result += templateHeader - .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); + result += templateHeader.render( + {"headers": padMultilineString(kJsonEncoder.convert(m), 2)}); } var templateBody = jj.Template(kTemplateBody); if (requestModel.hasFormData && requestModel.formDataMapList.isNotEmpty) { @@ -108,12 +108,13 @@ axios(config) : "fileInput$formFileCounter.files[0]"; if (element["type"] == "file") formFileCounter++; } - var sanitizedJSObject = sanitzeJSObject(kEncoder.convert(formParams)); + var sanitizedJSObject = + sanitzeJSObject(kJsonEncoder.convert(formParams)); result += templateBody .render({"body": padMultilineString(sanitizedJSObject, 2)}); } else if (harJson["postData"]?["text"] != null) { - result += templateBody - .render({"body": kEncoder.convert(harJson["postData"]["text"])}); + result += templateBody.render( + {"body": kJsonEncoder.convert(harJson["postData"]["text"])}); } result += kStringRequest; return result; diff --git a/lib/codegen/js/fetch.dart b/lib/codegen/js/fetch.dart index 21c4f238..c4ca14b0 100644 --- a/lib/codegen/js/fetch.dart +++ b/lib/codegen/js/fetch.dart @@ -106,7 +106,7 @@ fetch(url, options) } if (m.isNotEmpty) { result += templateHeader.render({ - "headers": padMultilineString(kEncoder.convert(m), 2), + "headers": padMultilineString(kJsonEncoder.convert(m), 2), }); } } @@ -114,7 +114,7 @@ fetch(url, options) if (harJson["postData"]?["text"] != null) { var templateBody = jj.Template(kTemplateBody); result += templateBody.render({ - "body": kEncoder.convert(harJson["postData"]["text"]), + "body": kJsonEncoder.convert(harJson["postData"]["text"]), }); } else if (requestModel.hasFormData) { var templateBody = jj.Template(kTemplateBody); diff --git a/lib/codegen/others/har.dart b/lib/codegen/others/har.dart index 32a35abe..acef6357 100644 --- a/lib/codegen/others/har.dart +++ b/lib/codegen/others/har.dart @@ -9,7 +9,7 @@ class HARCodeGen { String? boundary, }) { try { - var harString = kEncoder.convert(requestModelToHARJsonRequest( + var harString = kJsonEncoder.convert(requestModelToHARJsonRequest( requestModel, defaultUriScheme: defaultUriScheme, useEnabled: true, diff --git a/lib/codegen/python/http_client.dart b/lib/codegen/python/http_client.dart index ea783ebe..f58607c2 100644 --- a/lib/codegen/python/http_client.dart +++ b/lib/codegen/python/http_client.dart @@ -111,7 +111,7 @@ body = b'\r\n'.join(dataList) if (params.isNotEmpty) { hasQuery = true; var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); + var paramsString = kJsonEncoder.convert(params); result += templateParams.render({"params": paramsString}); } } @@ -143,7 +143,7 @@ body = b'\r\n'.join(dataList) }); } } - var headersString = kEncoder.convert(headers); + var headersString = kJsonEncoder.convert(headers); var templateHeaders = jj.Template(kTemplateHeaders); result += templateHeaders.render({"headers": headersString}); } diff --git a/lib/codegen/python/requests.dart b/lib/codegen/python/requests.dart index bb41ad9d..8ca4c152 100644 --- a/lib/codegen/python/requests.dart +++ b/lib/codegen/python/requests.dart @@ -107,7 +107,7 @@ print('Response Body:', response.text) if (params.isNotEmpty) { hasQuery = true; var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); + var paramsString = kJsonEncoder.convert(params); result += templateParams.render({"params": paramsString}); } } @@ -162,7 +162,7 @@ print('Response Body:', response.text) } if (headers.isNotEmpty) { hasHeaders = true; - var headersString = kEncoder.convert(headers); + var headersString = kJsonEncoder.convert(headers); headersString = refactorHeaderString(headersString); var templateHeaders = jj.Template(kTemplateHeaders); result += templateHeaders.render({"headers": headersString}); diff --git a/lib/consts.dart b/lib/consts.dart index ef8d74fc..685002af 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -415,7 +415,8 @@ enum ImportFormat { final String label; } -const JsonEncoder kEncoder = JsonEncoder.withIndent(' '); +const JsonEncoder kJsonEncoder = JsonEncoder.withIndent(' '); +const JsonDecoder kJsonDecoder = JsonDecoder(); const LineSplitter kSplitter = LineSplitter(); const String kGlobalEnvironmentId = "global"; @@ -745,6 +746,9 @@ const kLabelSaving = "Saving"; const kLabelSaved = "Saved"; const kLabelCode = "Code"; const kLabelDuplicate = "Duplicate"; +const kLabelSelect = "Select"; +const kLabelContinue = "Continue"; +const kLabelCancel = "Cancel"; // Request Pane const kLabelRequest = "Request"; const kLabelHideCode = "Hide Code"; @@ -779,3 +783,5 @@ const kNullResponseModelError = "Error: Response data does not exist."; const kMsgNullBody = "Response body is missing (null)."; const kMsgNoContent = "No content"; const kMsgUnknowContentType = "Unknown Response Content-Type"; +// Workspace Selector +const kMsgSelectWorkspace = "Create your workspace"; diff --git a/lib/importer/importer.dart b/lib/importer/importer.dart index b4388cd8..37c40a28 100644 --- a/lib/importer/importer.dart +++ b/lib/importer/importer.dart @@ -10,8 +10,6 @@ class Importer { switch (fileType) { case ImportFormat.curl: return CurlFileImport().getHttpRequestModel(content); - default: - return null; } } } diff --git a/lib/main.dart b/lib/main.dart index 4a8b07fd..7e212dad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,35 +1,82 @@ +import 'package:apidash/providers/settings_providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'models/models.dart'; +import 'providers/providers.dart'; import 'services/services.dart'; -import 'consts.dart' show kIsLinux, kIsMacOS, kIsWindows; +import 'consts.dart'; import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - - await initApp(); - await initWindow(); + var settingsModel = await getSettingsFromSharedPrefs(); + final initStatus = await initApp( + kIsDesktop, + settingsModel: settingsModel, + ); + if (kIsDesktop) { + await initWindow(settingsModel: settingsModel); + } + if (!initStatus) { + settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null); + } runApp( - const ProviderScope( - child: DashApp(), + ProviderScope( + overrides: [ + settingsProvider.overrideWith( + (ref) => ThemeStateNotifier(settingsModel: settingsModel), + ) + ], + child: const DashApp(), ), ); } -Future<void> initApp() async { +Future<bool> initApp( + bool initializeUsingPath, { + SettingsModel? settingsModel, +}) async { GoogleFonts.config.allowRuntimeFetching = false; - await openBoxes(); - await autoClearHistory(); + try { + debugPrint("initializeUsingPath: $initializeUsingPath"); + debugPrint("workspaceFolderPath: ${settingsModel?.workspaceFolderPath}"); + final openBoxesStatus = await initHiveBoxes( + initializeUsingPath, + settingsModel?.workspaceFolderPath, + ); + debugPrint("openBoxesStatus: $openBoxesStatus"); + if (openBoxesStatus) { + await autoClearHistory(settingsModel: settingsModel); + } + return openBoxesStatus; + } catch (e) { + debugPrint("initApp failed due to $e"); + return false; + } } -Future<void> initWindow({Size? sz}) async { +Future<void> initWindow({ + Size? sz, + SettingsModel? settingsModel, +}) async { if (kIsLinux) { - await setupInitialWindow(sz: sz); + await setupInitialWindow( + sz: sz ?? settingsModel?.size, + ); } if (kIsMacOS || kIsWindows) { - var win = sz != null ? (sz, const Offset(100, 100)) : getInitialSize(); - await setupWindow(sz: win.$1, off: win.$2); + if (sz != null) { + await setupWindow( + sz: sz, + off: const Offset(100, 100), + ); + } else { + await setupWindow( + sz: settingsModel?.size, + off: settingsModel?.offset, + ); + } } } diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index e82ffdca..6784f214 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -14,6 +14,7 @@ class SettingsModel { this.promptBeforeClosing = true, this.activeEnvironmentId, this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, + this.workspaceFolderPath, }); final bool isDark; @@ -26,6 +27,7 @@ class SettingsModel { final bool promptBeforeClosing; final String? activeEnvironmentId; final HistoryRetentionPeriod historyRetentionPeriod; + final String? workspaceFolderPath; SettingsModel copyWith({ bool? isDark, @@ -38,6 +40,7 @@ class SettingsModel { bool? promptBeforeClosing, String? activeEnvironmentId, HistoryRetentionPeriod? historyRetentionPeriod, + String? workspaceFolderPath, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -52,6 +55,25 @@ class SettingsModel { activeEnvironmentId: activeEnvironmentId ?? this.activeEnvironmentId, historyRetentionPeriod: historyRetentionPeriod ?? this.historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, + ); + } + + SettingsModel copyWithPath({ + String? workspaceFolderPath, + }) { + return SettingsModel( + isDark: isDark, + alwaysShowCollectionPaneScrollbar: alwaysShowCollectionPaneScrollbar, + size: size, + defaultUriScheme: defaultUriScheme, + defaultCodeGenLang: defaultCodeGenLang, + offset: offset, + saveResponses: saveResponses, + promptBeforeClosing: promptBeforeClosing, + activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath, ); } @@ -86,8 +108,7 @@ class SettingsModel { final promptBeforeClosing = data["promptBeforeClosing"] as bool?; final activeEnvironmentId = data["activeEnvironmentId"] as String?; final historyRetentionPeriodStr = data["historyRetentionPeriod"] as String?; - HistoryRetentionPeriod historyRetentionPeriod = - HistoryRetentionPeriod.oneWeek; + HistoryRetentionPeriod? historyRetentionPeriod; if (historyRetentionPeriodStr != null) { try { historyRetentionPeriod = @@ -96,6 +117,7 @@ class SettingsModel { // pass } } + final workspaceFolderPath = data["workspaceFolderPath"] as String?; const sm = SettingsModel(); @@ -109,7 +131,9 @@ class SettingsModel { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, - historyRetentionPeriod: historyRetentionPeriod, + historyRetentionPeriod: + historyRetentionPeriod ?? HistoryRetentionPeriod.oneWeek, + workspaceFolderPath: workspaceFolderPath, ); } @@ -127,12 +151,13 @@ class SettingsModel { "promptBeforeClosing": promptBeforeClosing, "activeEnvironmentId": activeEnvironmentId, "historyRetentionPeriod": historyRetentionPeriod.name, + "workspaceFolderPath": workspaceFolderPath, }; } @override String toString() { - return toJson().toString(); + return kJsonEncoder.convert(toJson()); } @override @@ -149,7 +174,8 @@ class SettingsModel { other.saveResponses == saveResponses && other.promptBeforeClosing == promptBeforeClosing && other.activeEnvironmentId == activeEnvironmentId && - other.historyRetentionPeriod == historyRetentionPeriod; + other.historyRetentionPeriod == historyRetentionPeriod && + other.workspaceFolderPath == workspaceFolderPath; } @override @@ -166,6 +192,7 @@ class SettingsModel { promptBeforeClosing, activeEnvironmentId, historyRetentionPeriod, + workspaceFolderPath, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 6293e04b..43256ab3 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/models.dart'; -import '../services/services.dart' show hiveHandler, HiveHandler; +import '../services/services.dart'; import '../consts.dart'; final codegenLanguageStateProvider = StateProvider<CodegenLanguage>((ref) => @@ -11,14 +11,13 @@ final activeEnvironmentIdStateProvider = StateProvider<String?>((ref) => ref.watch(settingsProvider.select((value) => value.activeEnvironmentId))); final StateNotifierProvider<ThemeStateNotifier, SettingsModel> - settingsProvider = - StateNotifierProvider((ref) => ThemeStateNotifier(hiveHandler)); + settingsProvider = StateNotifierProvider((ref) => ThemeStateNotifier()); class ThemeStateNotifier extends StateNotifier<SettingsModel> { - ThemeStateNotifier(this.hiveHandler) : super(const SettingsModel()) { - state = SettingsModel.fromJson(hiveHandler.settings); + ThemeStateNotifier({this.settingsModel}) : super(const SettingsModel()) { + state = settingsModel ?? const SettingsModel(); } - final HiveHandler hiveHandler; + final SettingsModel? settingsModel; Future<void> update({ bool? isDark, @@ -31,6 +30,7 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> { bool? promptBeforeClosing, String? activeEnvironmentId, HistoryRetentionPeriod? historyRetentionPeriod, + String? workspaceFolderPath, }) async { state = state.copyWith( isDark: isDark, @@ -43,7 +43,8 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> { promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, historyRetentionPeriod: historyRetentionPeriod, + workspaceFolderPath: workspaceFolderPath, ); - await hiveHandler.saveSettings(state.toJson()); + await setSettingsToSharedPrefs(state); } } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index 39011d81..86ac5a2f 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -17,6 +17,21 @@ class EditRequestBody extends ConsumerWidget { final contentType = ref.watch(selectedRequestModelProvider .select((value) => value?.httpRequestModel?.bodyContentType)); + // TODO: #178 GET->POST Currently switches to POST everytime user edits body even if the user intentionally chooses GET + // final sm = ScaffoldMessenger.of(context); + // void changeToPostMethod() { + // if (requestModel?.httpRequestModel!.method == HTTPVerb.get) { + // ref + // .read(collectionStateNotifierProvider.notifier) + // .update(selectedId, method: HTTPVerb.post); + // sm.hideCurrentSnackBar(); + // sm.showSnackBar(getSnackBar( + // "Switched to POST method", + // small: false, + // )); + // } + // } + return Column( children: [ const SizedBox( @@ -33,8 +48,12 @@ class EditRequestBody extends ConsumerWidget { ), Expanded( child: switch (contentType) { - ContentType.formdata => - const Padding(padding: kPh4, child: FormDataWidget()), + ContentType.formdata => const Padding( + padding: kPh4, + child: FormDataWidget( + // TODO: See changeToPostMethod above + // changeMethodToPost: changeToPostMethod, + )), // TODO: Fix JsonTextFieldEditor & plug it here ContentType.json => Padding( padding: kPt5o10, @@ -43,6 +62,7 @@ class EditRequestBody extends ConsumerWidget { fieldKey: "$selectedId-json-body-editor", initialValue: requestModel?.httpRequestModel?.body, onChanged: (String value) { + // changeToPostMethod(); ref .read(collectionStateNotifierProvider.notifier) .update(selectedId, body: value); @@ -56,6 +76,7 @@ class EditRequestBody extends ConsumerWidget { fieldKey: "$selectedId-body-editor", initialValue: requestModel?.httpRequestModel?.body, onChanged: (String value) { + // changeToPostMethod(); ref .read(collectionStateNotifierProvider.notifier) .update(selectedId, body: value); diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 8e9c8486..933b54b9 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; +import '../services/services.dart'; import '../widgets/widgets.dart'; import '../common/utils.dart'; import '../consts.dart'; @@ -206,6 +207,7 @@ class SettingsPage extends ConsumerWidget { TextButton( onPressed: () async { Navigator.pop(context, 'Yes'); + await clearSharedPrefs(); await ref .read(collectionStateNotifierProvider .notifier) diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart index d34d17fb..487b9164 100644 --- a/lib/services/history_service.dart +++ b/lib/services/history_service.dart @@ -1,18 +1,9 @@ import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; -import 'package:apidash/consts.dart'; import 'hive_services.dart'; -Future<void> autoClearHistory() async { - final settingsMap = hiveHandler.settings; - final retentionPeriod = settingsMap['historyRetentionPeriod']; - - HistoryRetentionPeriod historyRetentionPeriod = - HistoryRetentionPeriod.oneWeek; - if (retentionPeriod != null) { - historyRetentionPeriod = - HistoryRetentionPeriod.values.byName(retentionPeriod); - } +Future<void> autoClearHistory({SettingsModel? settingsModel}) async { + final historyRetentionPeriod = settingsModel?.historyRetentionPeriod; DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); if (retentionDate == null) { diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 1a0f7ac0..09ac8489 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; const String kDataBox = "apidash-data"; @@ -11,54 +11,95 @@ const String kHistoryMetaBox = "apidash-history-meta"; const String kHistoryBoxIds = "historyIds"; const String kHistoryLazyBox = "apidash-history-lazy"; -const String kSettingsBox = "apidash-settings"; - -Future<void> openBoxes() async { - await Hive.initFlutter(); - await Hive.openBox(kDataBox); - await Hive.openBox(kSettingsBox); - await Hive.openBox(kEnvironmentBox); - await Hive.openBox(kHistoryMetaBox); - await Hive.openLazyBox(kHistoryLazyBox); +Future<bool> initHiveBoxes( + bool initializeUsingPath, + String? workspaceFolderPath, +) async { + try { + if (initializeUsingPath) { + if (workspaceFolderPath != null) { + Hive.init(workspaceFolderPath); + } else { + return false; + } + } else { + await Hive.initFlutter(); + } + final openHiveBoxesStatus = await openHiveBoxes(); + return openHiveBoxesStatus; + } catch (e) { + return false; + } } -(Size?, Offset?) getInitialSize() { - Size? sz; - Offset? off; - var settingsBox = Hive.box(kSettingsBox); - double? w = settingsBox.get("width") as double?; - double? h = settingsBox.get("height") as double?; - if (w != null && h != null) { - sz = Size(w, h); +Future<bool> openHiveBoxes() async { + try { + await Hive.openBox(kDataBox); + await Hive.openBox(kEnvironmentBox); + await Hive.openBox(kHistoryMetaBox); + await Hive.openLazyBox(kHistoryLazyBox); + return true; + } catch (e) { + debugPrint("ERROR OPEN HIVE BOXES: $e"); + return false; } - double? dx = settingsBox.get("dx") as double?; - double? dy = settingsBox.get("dy") as double?; - if (dx != null && dy != null) { - off = Offset(dx, dy); +} + +Future<void> clearHiveBoxes() async { + try { + if (Hive.isBoxOpen(kDataBox)) { + await Hive.box(kDataBox).clear(); + } + if (Hive.isBoxOpen(kEnvironmentBox)) { + await Hive.box(kEnvironmentBox).clear(); + } + if (Hive.isBoxOpen(kHistoryMetaBox)) { + await Hive.box(kHistoryMetaBox).clear(); + } + if (Hive.isBoxOpen(kHistoryLazyBox)) { + await Hive.lazyBox(kHistoryLazyBox).clear(); + } + } catch (e) { + debugPrint("ERROR CLEAR HIVE BOXES: $e"); + } +} + +Future<void> deleteHiveBoxes() async { + try { + if (Hive.isBoxOpen(kDataBox)) { + await Hive.box(kDataBox).deleteFromDisk(); + } + if (Hive.isBoxOpen(kEnvironmentBox)) { + await Hive.box(kEnvironmentBox).deleteFromDisk(); + } + if (Hive.isBoxOpen(kHistoryMetaBox)) { + await Hive.box(kHistoryMetaBox).deleteFromDisk(); + } + if (Hive.isBoxOpen(kHistoryLazyBox)) { + await Hive.lazyBox(kHistoryLazyBox).deleteFromDisk(); + } + await Hive.close(); + } catch (e) { + debugPrint("ERROR DELETE HIVE BOXES: $e"); } - return (sz, off); } final hiveHandler = HiveHandler(); class HiveHandler { late final Box dataBox; - late final Box settingsBox; late final Box environmentBox; late final Box historyMetaBox; late final LazyBox historyLazyBox; HiveHandler() { + debugPrint("Trying to open Hive boxes"); dataBox = Hive.box(kDataBox); - settingsBox = Hive.box(kSettingsBox); environmentBox = Hive.box(kEnvironmentBox); historyMetaBox = Hive.box(kHistoryMetaBox); historyLazyBox = Hive.lazyBox(kHistoryLazyBox); } - Map get settings => settingsBox.toMap(); - Future<void> saveSettings(Map data) => settingsBox.putAll(data); - dynamic getIds() => dataBox.get(kKeyDataBoxIds); Future<void> setIds(List<String>? ids) => dataBox.put(kKeyDataBoxIds, ids); @@ -102,6 +143,8 @@ class HiveHandler { Future clear() async { await dataBox.clear(); await environmentBox.clear(); + await historyMetaBox.clear(); + await historyLazyBox.clear(); } Future<void> removeUnused() async { diff --git a/lib/services/services.dart b/lib/services/services.dart index 7551de9b..fd8ca0b0 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -2,3 +2,4 @@ export 'http_service.dart'; export 'hive_services.dart'; export 'history_service.dart'; export 'window_services.dart'; +export 'shared_preferences_services.dart'; diff --git a/lib/services/shared_preferences_services.dart b/lib/services/shared_preferences_services.dart new file mode 100644 index 00000000..89d4b590 --- /dev/null +++ b/lib/services/shared_preferences_services.dart @@ -0,0 +1,28 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const String kSharedPrefSettingsKey = 'apidash-settings'; + +Future<SettingsModel?> getSettingsFromSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + var settingsStr = prefs.getString(kSharedPrefSettingsKey); + if (settingsStr != null) { + var jsonSettings = kJsonDecoder.convert(settingsStr); + var jsonMap = Map<String, Object?>.from(jsonSettings); + var settingsModel = SettingsModel.fromJson(jsonMap); + return settingsModel; + } else { + return null; + } +} + +Future<void> setSettingsToSharedPrefs(SettingsModel settingsModel) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(kSharedPrefSettingsKey, settingsModel.toString()); +} + +Future<void> clearSharedPrefs() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(kSharedPrefSettingsKey); +} diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index d0b2ac26..b3c3b2bc 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -150,7 +150,7 @@ Uint8List jsonMapToBytes(Map<String, dynamic>? map) { if (map == null) { return Uint8List.fromList([]); } else { - String text = kEncoder.convert(map); + String text = kJsonEncoder.convert(map); var l = utf8.encode(text); var bytes = Uint8List.fromList(l); return bytes; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index cce87998..e6db9d38 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -114,7 +114,7 @@ List<HistoryMetaModel> getRequestGroup( return requestGroup; } -DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { +DateTime? getRetentionDate(HistoryRetentionPeriod? retentionPeriod) { DateTime now = DateTime.now(); DateTime today = stripTime(now); diff --git a/lib/utils/http_utils.dart b/lib/utils/http_utils.dart index 4688d2af..e12f029f 100644 --- a/lib/utils/http_utils.dart +++ b/lib/utils/http_utils.dart @@ -101,33 +101,34 @@ String stripUrlParams(String url) { (List<ResponseBodyView>, String?) getResponseBodyViewOptions( MediaType? mediaType) { - if (mediaType != null) { - var type = mediaType.type; - var subtype = mediaType.subtype; - if (kResponseBodyViewOptions.containsKey(type)) { - if (kResponseBodyViewOptions[type]!.containsKey(subtype)) { - return ( - kResponseBodyViewOptions[type]![subtype]!, - kCodeHighlighterMap[subtype] ?? subtype - ); - } - if (subtype.contains(kSubTypeJson)) { - subtype = kSubTypeJson; - } - if (subtype.contains(kSubTypeXml)) { - subtype = kSubTypeXml; - } - if (kResponseBodyViewOptions[type]!.containsKey(subtype)) { - return ( - kResponseBodyViewOptions[type]![subtype]!, - kCodeHighlighterMap[subtype] ?? subtype - ); - } + if (mediaType == null) { + return (kRawBodyViewOptions, null); + } + var type = mediaType.type; + var subtype = mediaType.subtype; + if (kResponseBodyViewOptions.containsKey(type)) { + if (kResponseBodyViewOptions[type]!.containsKey(subtype)) { return ( - kResponseBodyViewOptions[type]![kSubTypeDefaultViewOptions]!, - subtype + kResponseBodyViewOptions[type]![subtype]!, + kCodeHighlighterMap[subtype] ?? subtype ); } + if (subtype.contains(kSubTypeJson)) { + subtype = kSubTypeJson; + } + if (subtype.contains(kSubTypeXml)) { + subtype = kSubTypeXml; + } + if (kResponseBodyViewOptions[type]!.containsKey(subtype)) { + return ( + kResponseBodyViewOptions[type]![subtype]!, + kCodeHighlighterMap[subtype] ?? subtype + ); + } + return ( + kResponseBodyViewOptions[type]![kSubTypeDefaultViewOptions]!, + subtype + ); } return (kNoBodyViewOptions, null); } @@ -138,7 +139,7 @@ String? formatBody(String? body, MediaType? mediaType) { try { if (subtype.contains(kSubTypeJson)) { final tmp = jsonDecode(body); - String result = kEncoder.convert(tmp); + String result = kJsonEncoder.convert(tmp); return result; } if (subtype.contains(kSubTypeXml)) { diff --git a/lib/widgets/field_outlined.dart b/lib/widgets/field_outlined.dart new file mode 100644 index 00000000..0c981543 --- /dev/null +++ b/lib/widgets/field_outlined.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class OutlinedField extends StatelessWidget { + const OutlinedField({ + super.key, + 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 TextFormField( + key: keyId != null ? Key(keyId!) : null, + initialValue: initialValue, + style: kCodeStyle.copyWith( + color: clrScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: clrScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: hintText, + contentPadding: kP10, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + isDense: true, + ), + onChanged: onChanged, + ); + } +} diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 3a4f5648..020015d6 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -182,7 +182,7 @@ class _JsonPreviewerState extends State<JsonPreviewer> { TextButton( onPressed: () async { await _copy( - kEncoder.convert(widget.code), sm); + kJsonEncoder.convert(widget.code), sm); }, child: const Text( 'Copy', @@ -215,7 +215,7 @@ class _JsonPreviewerState extends State<JsonPreviewer> { visualDensity: VisualDensity.compact, onPressed: () async { await _copy( - kEncoder.convert(widget.code), sm); + kJsonEncoder.convert(widget.code), sm); }, icon: const Icon( Icons.copy, @@ -273,7 +273,7 @@ class _JsonPreviewerState extends State<JsonPreviewer> { ), onPressed: () async { await _copy( - kEncoder.convert(toJson(node)), sm); + kJsonEncoder.convert(toJson(node)), sm); }, ), ) diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 0df9343b..7b269fc1 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -262,7 +262,7 @@ class ResponseHeadersHeader extends StatelessWidget { ), if (map.isNotEmpty) CopyButton( - toCopy: kEncoder.convert(map), + toCopy: kJsonEncoder.convert(map), ), ], ), @@ -345,12 +345,14 @@ class ResponseBody extends StatelessWidget { ); } - var mediaType = responseModel.mediaType; - if (mediaType == null) { - return ErrorMessage( - message: - '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); - } + final mediaType = + responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); + // Fix #415: Treat null Content-type as plain text instead of Error message + // if (mediaType == null) { + // return ErrorMessage( + // message: + // '$kMsgUnknowContentType - ${responseModel.contentType}. $kUnexpectedRaiseIssue'); + // } var responseBodyView = getResponseBodyViewOptions(mediaType); var options = responseBodyView.$1; diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 06bc4bef..ebf56e8e 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -31,6 +31,7 @@ export 'field_cell_obscurable.dart'; export 'field_cell.dart'; export 'field_header.dart'; export 'field_json_search.dart'; +export 'field_outlined.dart'; export 'field_raw.dart'; export 'field_read_only.dart'; export 'field_url.dart'; @@ -60,3 +61,4 @@ export 'tabs.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; export 'window_caption.dart'; +export 'workspace_selector.dart'; diff --git a/lib/widgets/workspace_selector.dart b/lib/widgets/workspace_selector.dart new file mode 100644 index 00000000..2d41b311 --- /dev/null +++ b/lib/widgets/workspace_selector.dart @@ -0,0 +1,133 @@ +import 'package:apidash/consts.dart'; +import 'package:flutter/material.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:path/path.dart' as p; +import 'field_outlined.dart'; + +class WorkspaceSelector extends HookWidget { + const WorkspaceSelector({ + super.key, + required this.onContinue, + this.onCancel, + }); + + final Future<void> Function(String)? onContinue; + final Future<void> Function()? onCancel; + + @override + Widget build(BuildContext context) { + var selectedDirectory = useState<String?>(null); + var workspaceName = useState<String?>(null); + return Scaffold( + body: Center( + child: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + kMsgSelectWorkspace, + style: kTextStyleButton, + ), + kVSpacer20, + Row( + children: [ + Text( + "CHOOSE DIRECTORY", + style: kCodeStyle.copyWith( + fontSize: 12, + ), + ), + ], + ), + kVSpacer5, + Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.primaryContainer, + ), + borderRadius: kBorderRadius6, + ), + padding: kP4, + child: Text( + style: kTextStyleButtonSmall, + selectedDirectory.value ?? "", + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ), + ), + kHSpacer10, + FilledButton.tonalIcon( + onPressed: () async { + selectedDirectory.value = await getDirectoryPath(); + }, + label: const Text(kLabelSelect), + icon: const Icon(Icons.folder_rounded), + ), + ], + ), + kVSpacer10, + Row( + children: [ + Text( + "WORKSPACE NAME [OPTIONAL]\n(FOLDER WILL BE CREATED IN THE SELECTED DIRECTORY)", + style: kCodeStyle.copyWith( + fontSize: 12, + ), + ), + ], + ), + kVSpacer5, + OutlinedField( + keyId: "workspace-name", + onChanged: (value) { + workspaceName.value = value.trim(); + }, + colorScheme: Theme.of(context).colorScheme, + ), + kVSpacer40, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton( + onPressed: selectedDirectory.value == null + ? null + : () async { + String finalPath = selectedDirectory.value!; + if (workspaceName.value != null && + workspaceName.value!.trim().isNotEmpty) { + finalPath = + p.join(finalPath, workspaceName.value); + } + await onContinue?.call(finalPath); + }, + child: const Text(kLabelContinue), + ), + kHSpacer10, + FilledButton( + onPressed: onCancel, + style: FilledButton.styleFrom( + backgroundColor: + Theme.of(context).brightness == Brightness.dark + ? kColorDarkDanger + : kColorLightDanger, + surfaceTintColor: kColorRed, + foregroundColor: + Theme.of(context).colorScheme.onPrimary), + child: const Text(kLabelCancel), + ) + ], + ) + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 696e9f0a..779555b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -259,10 +267,10 @@ packages: dependency: "direct main" description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartx: dependency: transitive description: @@ -521,6 +529,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3+1" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + url: "https://pub.dev" + source: hosted + version: "2.4.1" flutter_portal: dependency: "direct main" description: @@ -596,10 +612,10 @@ packages: dependency: "direct main" description: name: fvp - sha256: "6462fd078de4478a0990d437463897036cff98aff3f0ac9efbbf817c99654c87" + sha256: "040aa12beccd5bc60631259f27a481c4abc11a389aa4f57a47b643f58fe0b060" url: "https://pub.dev" source: hosted - version: "0.24.1" + version: "0.26.1" glob: dependency: transitive description: @@ -778,10 +794,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: ee50602364ba83fa6308f5512dd560c713ec3e1f2bc75f0db43618f0d82ef71a + sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d url: "https://pub.dev" source: hosted - version: "0.9.39" + version: "0.9.40" just_audio_mpv: dependency: "direct main" description: @@ -1215,6 +1231,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -1428,6 +1500,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -1496,10 +1576,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_graphics: dependency: transitive description: @@ -1671,4 +1751,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.0-259.0.dev <3.999.0" - flutter: ">=3.22.0" + flutter: ">=3.24.2" diff --git a/pubspec.yaml b/pubspec.yaml index 0eadf529..66dde1e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.4.0+4 environment: sdk: ">=3.0.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.24.2" dependencies: flutter: @@ -18,7 +18,7 @@ dependencies: url: https://github.com/foss42/curl_converter.git ref: 726e8cd04aeb326211af27f75920be5b21c90bb4 data_table_2: ^2.5.15 - dart_style: ^2.3.6 + dart_style: ^2.3.7 desktop_drop: ^0.4.4 extended_text_field: ^16.0.0 file_selector: ^1.0.3 @@ -30,7 +30,7 @@ dependencies: flutter_svg: ^2.0.10+1 flutter_typeahead: ^5.2.0 freezed_annotation: ^2.4.1 - fvp: ^0.24.1 + fvp: ^0.26.1 google_fonts: ^6.2.1 highlighter: ^0.1.1 hive_flutter: ^1.1.0 @@ -45,7 +45,7 @@ dependencies: url: https://github.com/foss42/json_data_explorer.git ref: b7dde2f85dff4f482eed7eda4ef2a71344ef8b3a json_text_field: ^1.2.0 - just_audio: ^0.9.34 + just_audio: ^0.9.40 just_audio_mpv: ^0.1.7 just_audio_windows: ^0.2.0 lottie: ^3.1.0 @@ -63,8 +63,9 @@ dependencies: provider: ^6.1.2 riverpod: ^2.5.1 scrollable_positioned_list: ^0.3.8 + shared_preferences: ^2.3.2 url_launcher: ^6.2.5 - uuid: ^4.3.3 + uuid: ^4.5.0 vector_graphics_compiler: ^1.1.9+1 video_player: ^2.8.7 video_player_platform_interface: ^6.2.2 @@ -86,6 +87,7 @@ dev_dependencies: build_runner: ^2.4.12 flutter_launcher_icons: ^0.13.1 flutter_lints: ^4.0.0 + flutter_native_splash: ^2.4.1 freezed: ^2.5.7 json_serializable: ^6.7.1 integration_test: diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 6df01335..0a1a33a9 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -15,6 +15,7 @@ void main() { promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, + workspaceFolderPath: null, ); test('Testing toJson()', () { @@ -31,6 +32,7 @@ void main() { "promptBeforeClosing": true, "activeEnvironmentId": null, "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null, }; expect(sm.toJson(), expectedResult); }); @@ -49,6 +51,7 @@ void main() { "promptBeforeClosing": true, "activeEnvironmentId": null, "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null, }; expect(SettingsModel.fromJson(input), sm); }); @@ -75,8 +78,21 @@ void main() { }); test('Testing toString()', () { - const expectedResult = - "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: oneWeek}"; + const expectedResult = '''{ + "isDark": false, + "alwaysShowCollectionPaneScrollbar": true, + "width": 300.0, + "height": 200.0, + "dx": 100.0, + "dy": 150.0, + "defaultUriScheme": "http", + "defaultCodeGenLang": "curl", + "saveResponses": true, + "promptBeforeClosing": true, + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", + "workspaceFolderPath": null +}'''; expect(sm.toString(), expectedResult); }); diff --git a/test/providers/collection_providers_test.dart b/test/providers/collection_providers_test.dart new file mode 100644 index 00000000..fb939522 --- /dev/null +++ b/test/providers/collection_providers_test.dart @@ -0,0 +1,54 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/request_body.dart'; +import 'package:apidash/widgets/editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'helpers.dart'; + +void main() async { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + await testSetUpTempDirForHive(); + }); + + testWidgets( + 'Request method changes from GET to POST when body is added and Snackbar is shown', + (WidgetTester tester) async { + // Set up the test environment + final container = createContainer(); + final notifier = container.read(collectionStateNotifierProvider.notifier); + + // Ensure the initial request is a GET request with no body + final id = notifier.state!.entries.first.key; + expect( + notifier.getRequestModel(id)!.httpRequestModel!.method, HTTPVerb.get); + expect(notifier.getRequestModel(id)!.httpRequestModel!.body, isNull); + + // Build the EditRequestBody widget + await tester.pumpWidget( + ProviderScope( + // ignore: deprecated_member_use + parent: container, + child: const MaterialApp( + home: Scaffold( + body: EditRequestBody(), + ), + ), + ), + ); + + // Add a body to the request, which should trigger the method change + await tester.enterText(find.byType(TextFieldEditor), 'new body added'); + await tester.pump(); // Process the state change + + // Verify that the request method changed to POST + expect( + notifier.getRequestModel(id)!.httpRequestModel!.method, HTTPVerb.post); + + // Verify that the Snackbar is shown + expect(find.text('Switched to POST method'), findsOneWidget); + }, skip: true); +} diff --git a/test/providers/helpers.dart b/test/providers/helpers.dart new file mode 100644 index 00000000..568a849c --- /dev/null +++ b/test/providers/helpers.dart @@ -0,0 +1,57 @@ +import 'dart:io'; +import 'package:apidash/services/hive_services.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// A testing utility which creates a [ProviderContainer] and automatically +/// disposes it at the end of the test. +ProviderContainer createContainer({ + ProviderContainer? parent, + List<Override> overrides = const [], + List<ProviderObserver>? observers, +}) { + // Create a ProviderContainer, and optionally allow specifying parameters. + final container = ProviderContainer( + parent: parent, + overrides: overrides, + observers: observers, + ); + + // When the test ends, dispose the container. + addTearDown(container.dispose); + + return container; +} + +Future<void> testSetUpForHive() async { + // override path_provider methodCall to point + // path to temporary location for all unit tests + const MethodChannel channel = + MethodChannel('plugins.flutter.io/path_provider'); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return './test-hive-storage/'; + }); + + await initHiveBoxes(false, null); + // await deleteHiveBoxes(); + // await openHiveBoxes(); +} + +Future<void> testSetUpTempDirForHive() async { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/path_provider'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == 'getApplicationDocumentsDirectory') { + // Create a mock app doc directory for testing + Directory tempDir = + await Directory.systemTemp.createTemp('mock_app_doc_dir'); + return tempDir.path; // Return the path to the mock directory + } + return null; + }); + await initHiveBoxes(false, null); +} diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 39ef1ff6..e8fd0582 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,4 +1,3 @@ -import 'dart:io'; //import 'package:spot/spot.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; @@ -12,7 +11,6 @@ import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/screens/home_page/home_page.dart'; import 'package:apidash/screens/settings_page.dart'; import 'package:apidash/screens/history/history_page.dart'; -import 'package:apidash/services/hive_services.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; @@ -22,24 +20,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import '../extensions/widget_tester_extensions.dart'; import '../test_consts.dart'; +import 'helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/path_provider'); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - if (methodCall.method == 'getApplicationDocumentsDirectory') { - // Create a mock app doc directory for testing - Directory tempDir = - await Directory.systemTemp.createTemp('mock_app_doc_dir'); - return tempDir.path; // Return the path to the mock directory - } - return null; - }); - await openBoxes(); + await testSetUpTempDirForHive(); final flamante = rootBundle.load('google_fonts/OpenSans-Medium.ttf'); final fontLoader = FontLoader('OpenSans')..addFont(flamante); await fontLoader.load(); diff --git a/test/widgets/response_widgets_test.dart b/test/widgets/response_widgets_test.dart index 1c3c4bef..74df9a1b 100644 --- a/test/widgets/response_widgets_test.dart +++ b/test/widgets/response_widgets_test.dart @@ -214,8 +214,13 @@ void main() { findsOneWidget); }); - testWidgets('Testing Response Body, no mediaType', (tester) async { - var responseModelNoHeaders = responseModel.copyWith(headers: null); + testWidgets( + 'Testing Response Body, no mediaType; shoud be default plaintext preview', + (tester) async { + var responseModelNoHeaders = responseModel.copyWith( + headers: null, + formattedBody: null, + ); var requestModelNoResponseHeaders = testRequestModel.copyWith(httpResponseModel: responseModelNoHeaders); @@ -230,10 +235,8 @@ void main() { ), ); - expect( - find.text( - 'Unknown Response Content-Type - ${responseModelNoHeaders.contentType}. $kUnexpectedRaiseIssue'), - findsOneWidget); + expect(find.text("Raw"), findsOneWidget); + expect(find.text('{"data":"world"}'), findsOneWidget); }); testWidgets('Testing Response Body for No body view', (tester) async { diff --git a/test/widgets/table_request_form_test.dart b/test/widgets/table_request_form_test.dart index a1c67424..c21a9b70 100644 --- a/test/widgets/table_request_form_test.dart +++ b/test/widgets/table_request_form_test.dart @@ -6,6 +6,7 @@ import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; void main() { + dataTableShowLogs = false; testWidgets('Testing RequestFormDataTable', (WidgetTester tester) async { const List<FormDataModel> sampleData = [ FormDataModel(name: 'Key1', value: 'Value1', type: FormDataType.file), diff --git a/test/widgets/table_request_test.dart b/test/widgets/table_request_test.dart index 6e8c2c9f..cc0e3324 100644 --- a/test/widgets/table_request_test.dart +++ b/test/widgets/table_request_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + dataTableShowLogs = false; testWidgets('Testing RequestDataTable', (WidgetTester tester) async { final Map<String, String> sampleData = { 'Key1': 'Value1',