diff --git a/.gitignore b/.gitignore index 9268964a..4bdfd329 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ macos/ windows/ web/ ios/ +android/ .vscode/* icons/ coverage/* diff --git a/README.md b/README.md index d26478e4..f67c8f17 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Here is the complete list of mimetypes that can be directly previewed in API Das | Image | `image/portable-anymap` | `.pnm` | | | Image | `image/png` | `.png` | | | Image | `image/sgi` | `.sgi` | | -| Image | `image/svg+xml` | `.svg` | Partial support. See issue https://github.com/foss42/apidash/issues/20 | +| Image | `image/svg+xml` | `.svg` | | | Image | `image/tiff` | `.tiff` | | | Image | `image/targa` | `.tga` | | | Image | `image/vnd.wap.wbmp` | `.wbmp` | | diff --git a/lib/app.dart b/lib/app.dart index ae70328c..e6999c01 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,10 +1,10 @@ +import 'package:apidash/widgets/window_caption.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; +import 'package:window_manager/window_manager.dart' hide WindowCaption; import 'providers/providers.dart'; import 'screens/screens.dart'; -import 'consts.dart' - show kIsLinux, kFontFamily, kFontFamilyFallback, kColorSchemeSeed; +import 'consts.dart'; class App extends ConsumerStatefulWidget { const App({super.key}); @@ -64,6 +64,7 @@ class _DashAppState extends ConsumerState { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, fontFamily: kFontFamily, fontFamilyFallback: kFontFamilyFallback, colorSchemeSeed: kColorSchemeSeed, @@ -78,7 +79,24 @@ class _DashAppState extends ConsumerState { brightness: Brightness.dark, ), themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, - home: kIsLinux ? const Dashboard() : const App(), + home: kIsMobile + ? const MobileDashboard( + title: 'Requests', + scaffoldBody: CollectionPane(), + ) + : Stack( + children: [ + kIsLinux ? const Dashboard() : const App(), + if (kIsWindows) + SizedBox( + height: 29, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: isDarkMode ? Brightness.dark : Brightness.light, + ), + ), + ], + ), ); } } diff --git a/lib/consts.dart b/lib/consts.dart index 25d93569..895c0460 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -16,6 +16,10 @@ final kIsApple = !kIsWeb && (Platform.isIOS || Platform.isMacOS); final kIsDesktop = !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux); +final kIsIOS = !kIsWeb && Platform.isIOS; +final kIsAndroid = !kIsWeb && Platform.isAndroid; +final kIsMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid); + final kColorTransparentState = MaterialStateProperty.all(Colors.transparent); const kColorTransparent = Colors.transparent; @@ -338,7 +342,7 @@ const Map>> }, kTypeImage: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, - kSubTypeSvg: kCodeRawBodyViewOptions, + kSubTypeSvg: kPreviewRawBodyViewOptions, }, kTypeAudio: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, @@ -455,6 +459,9 @@ const kUnexpectedRaiseIssue = const kImageError = "There seems to be an issue rendering this image. Please raise an issue in API Dash GitHub repo so that we can resolve it."; +const kSvgError = + "There seems to be an issue rendering this SVG image. Please raise an issue in API Dash GitHub repo so that we can resolve it."; + const kPdfError = "There seems to be an issue rendering this pdf. Please raise an issue in API Dash GitHub repo so that we can resolve it."; diff --git a/lib/main.dart b/lib/main.dart index f99a58d5..5ac878aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'services/services.dart'; -import 'consts.dart' show kIsLinux; +import 'consts.dart' show kIsLinux, kIsMacOS, kIsWindows; import 'app.dart'; void main() async { @@ -11,7 +11,8 @@ void main() async { await openBoxes(); if (kIsLinux) { await setupInitialWindow(); - } else { + } + if (kIsMacOS || kIsWindows) { var win = getInitialSize(); await setupWindow(sz: win.$1, off: win.$2); } diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 9a391279..ec7d87d0 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -27,7 +27,7 @@ class _RequestEditorPaneState extends ConsumerState { return const RequestEditorDefault(); } else { return Padding( - padding: kIsMacOS ? kPt24o8 : kP8, + padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8, child: const Column( children: [ EditorPaneRequestURLCard(), diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart new file mode 100644 index 00000000..b6f25041 --- /dev/null +++ b/lib/screens/mobile/dashboard.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../intro_page.dart'; +import '../settings_page.dart'; +import '../home_page/collection_pane.dart'; + +class MobileDashboard extends ConsumerStatefulWidget { + const MobileDashboard( + {required this.scaffoldBody, required this.title, super.key}); + + final Widget scaffoldBody; + final String title; + + @override + ConsumerState createState() => _MobileDashboardState(); +} + +class _MobileDashboardState extends ConsumerState { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const SizedBox( + height: 70, + ), + ListTile( + title: const Text('Home'), + leading: const Icon(Icons.home_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Home', + scaffoldBody: IntroPage(), + ), + ), + (Route route) => false); + }, + ), + ListTile( + title: const Text('Requests'), + leading: const Icon(Icons.auto_awesome_mosaic_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Requests', + scaffoldBody: CollectionPane(), + ), + ), + (Route route) => false); + }, + ), + ListTile( + title: const Text('Settings'), + leading: const Icon(Icons.settings_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Settings', + scaffoldBody: SettingsPage(), + ), + ), + (Route route) => false); + }, + ), + const Divider(), + ], + ), + ), + body: SafeArea( + child: widget.scaffoldBody, + ), + ); + } +} diff --git a/lib/screens/mobile/mobile.dart b/lib/screens/mobile/mobile.dart new file mode 100644 index 00000000..1b8c3333 --- /dev/null +++ b/lib/screens/mobile/mobile.dart @@ -0,0 +1 @@ +export 'dashboard.dart'; diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart index c3c70a99..c647c90d 100644 --- a/lib/screens/screens.dart +++ b/lib/screens/screens.dart @@ -1 +1,3 @@ -export "dashboard.dart"; +export 'dashboard.dart'; +export 'mobile/mobile.dart'; +export 'home_page/collection_pane.dart'; diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 28e93658..e1249812 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -27,9 +27,11 @@ class _SettingsPageState extends ConsumerState { padding: kPh20t40, shrinkWrap: true, children: [ - Text("Settings", - style: Theme.of(context).textTheme.headlineLarge), - const Divider(), + kIsDesktop + ? Text("Settings", + style: Theme.of(context).textTheme.headlineLarge) + : const SizedBox.shrink(), + kIsDesktop ? const Divider() : const SizedBox.shrink(), SwitchListTile( contentPadding: EdgeInsets.zero, hoverColor: kColorTransparent, diff --git a/lib/services/window_services.dart b/lib/services/window_services.dart index 60b13f50..0b430e53 100644 --- a/lib/services/window_services.dart +++ b/lib/services/window_services.dart @@ -62,7 +62,7 @@ Future setupWindow({Size? sz, Offset? off, bool center = false}) async { minimumSize: kMinWindowSize, skipTaskbar: false, title: kWindowTitle, - titleBarStyle: kIsMacOS ? TitleBarStyle.hidden : null, + titleBarStyle: kIsMacOS || kIsWindows ? TitleBarStyle.hidden : null, ); if (off != null) { await windowManager.setPosition(off); diff --git a/lib/widgets/previewer.dart b/lib/widgets/previewer.dart index db7d9edb..9841ac5b 100644 --- a/lib/widgets/previewer.dart +++ b/lib/widgets/previewer.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'error_message.dart'; -import 'package:apidash/consts.dart'; import 'package:printing/printing.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; +import 'error_message.dart'; import 'uint8_audio_player.dart'; import 'json_previewer.dart'; +import '../consts.dart'; class Previewer extends StatefulWidget { const Previewer({ @@ -40,6 +42,18 @@ class _PreviewerState extends State { // pass } } + if (widget.type == kTypeImage && widget.subtype == kSubTypeSvg) { + final String rawSvg = widget.body; + try { + parseWithoutOptimizers(rawSvg); + var svgImg = SvgPicture.string( + rawSvg, + ); + return svgImg; + } catch (e) { + return const ErrorMessage(message: kSvgError); + } + } if (widget.type == kTypeImage) { return Image.memory( widget.bytes, diff --git a/lib/widgets/window_caption.dart b/lib/widgets/window_caption.dart new file mode 100644 index 00000000..f26e7539 --- /dev/null +++ b/lib/widgets/window_caption.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +import 'package:window_manager/window_manager.dart'; + +const double kWindowCaptionHeight = 30; + +class WindowCaption extends StatefulWidget { + const WindowCaption({ + super.key, + this.backgroundColor, + this.brightness, + }); + + final Color? backgroundColor; + final Brightness? brightness; + + @override + State createState() => _WindowCaptionState(); +} + +class _WindowCaptionState extends State with WindowListener { + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanStart: (details) { + windowManager.startDragging(); + }, + child: const SizedBox( + height: double.infinity, + ), + ), + ), + WindowCaptionButton.minimize( + brightness: widget.brightness, + onPressed: () async { + bool isMinimized = await windowManager.isMinimized(); + if (isMinimized) { + windowManager.restore(); + } else { + windowManager.minimize(); + } + }, + ), + FutureBuilder( + future: windowManager.isMaximized(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == true) { + return WindowCaptionButton.unmaximize( + brightness: widget.brightness, + onPressed: () { + windowManager.unmaximize(); + }, + ); + } + return WindowCaptionButton.maximize( + brightness: widget.brightness, + onPressed: () { + windowManager.maximize(); + }, + ); + }, + ), + WindowCaptionButton.close( + brightness: widget.brightness, + onPressed: () { + windowManager.close(); + }, + ), + ], + ); + } + + @override + void onWindowMaximize() { + setState(() {}); + } + + @override + void onWindowUnmaximize() { + setState(() {}); + } +} diff --git a/pubspec.lock b/pubspec.lock index 5c3ddfc8..246dc90d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -374,6 +374,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -1174,6 +1182,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_compiler: + dependency: "direct main" + description: + name: vector_graphics_compiler + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0adf1380..419b6397 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,8 @@ dependencies: url: https://github.com/foss42/json_data_explorer.git version: ^0.1.1 scrollable_positioned_list: ^0.2.3 + flutter_svg: ^2.0.9 + vector_graphics_compiler: ^1.1.9+1 json_text_field: ^1.0.0 dev_dependencies: diff --git a/test/utils/http_utils_test.dart b/test/utils/http_utils_test.dart index 2cc04763..55129bcc 100644 --- a/test/utils/http_utils_test.dart +++ b/test/utils/http_utils_test.dart @@ -268,7 +268,7 @@ void main() { test('Testing getResponseBodyViewOptions for image/svg+xml', () { MediaType mediaType5 = MediaType("image", "svg+xml"); var result5 = getResponseBodyViewOptions(mediaType5); - expect(result5.$1, kCodeRawBodyViewOptions); + expect(result5.$1, kPreviewRawBodyViewOptions); expect(result5.$2, "xml"); }); test('Testing getResponseBodyViewOptions for application/xhtml+xml', () { diff --git a/test/widgets/previewer_test.dart b/test/widgets/previewer_test.dart index c98a9070..0d2f68cd 100644 --- a/test/widgets/previewer_test.dart +++ b/test/widgets/previewer_test.dart @@ -4,6 +4,7 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'package:flutter/foundation.dart'; import 'package:printing/printing.dart' show PdfPreview; +import 'package:flutter_svg/flutter_svg.dart' show SvgPicture; import '../test_consts.dart'; void main() { @@ -169,4 +170,65 @@ void main() { await tester.pumpAndSettle(); expect(find.text(kAudioError), findsOneWidget); }); + + testWidgets('Testing when type/subtype is image/svg+xml', (tester) async { + String rawSvg = + """ + + + + + + + + + + + + + + + + + + +"""; + + await tester.pumpWidget( + MaterialApp( + title: 'Previewer', + home: Scaffold( + body: Previewer( + type: 'image', + subtype: 'svg+xml', + bytes: Uint8List.fromList([]), + body: rawSvg, + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text(kSvgError), findsNothing); + expect(find.byType(SvgPicture), findsOneWidget); + }); + + testWidgets('Testing when type/subtype is image/svg+xml corrupted', + (tester) async { + String rawSvg = "rwsjhdws"; + await tester.pumpWidget( + MaterialApp( + title: 'Previewer', + home: Scaffold( + body: Previewer( + type: 'image', + subtype: 'svg+xml', + bytes: Uint8List.fromList([]), + body: rawSvg, + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text(kSvgError), findsOneWidget); + }); }