diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 62cb12ec..706a8ab3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,6 +57,8 @@ PODS: - SDWebImage (5.13.1): - SDWebImage/Core (= 5.13.1) - SDWebImage/Core (5.13.1) + - share_plus (0.0.1): + - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -78,6 +80,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -111,6 +114,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: @@ -133,6 +138,7 @@ SPEC CHECKSUMS: path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 SDWebImage: fb26a455eeda4c7a55e4dcb6172dbb258af7a4ca + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 @@ -140,4 +146,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a62623f56f2d1d0e85a4a3c73509cd2832d5c86f -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.0 diff --git a/packages/features/workspace_screen/lib/src/components/drawing_surface/drawing_canvas.dart b/packages/features/workspace_screen/lib/src/components/drawing_surface/drawing_canvas.dart index 23c5d02e..8b491c76 100644 --- a/packages/features/workspace_screen/lib/src/components/drawing_surface/drawing_canvas.dart +++ b/packages/features/workspace_screen/lib/src/components/drawing_surface/drawing_canvas.dart @@ -14,7 +14,7 @@ class _DrawingCanvasState extends ConsumerState { late final _toolBoxStateNotifier = ref.read(toolBoxStateProvider.notifier); late final _canvasStateNotifier = ref.read(canvasStateProvider.notifier); late final _canvasDirtyNotifier = - ref.read(CanvasDirtyState.provider.notifier); + ref.read(CanvasDirtyState.provider.notifier); final _canvasPainterKey = GlobalKey(debugLabel: 'CanvasPainter'); final _transformationController = TransformationController(); @@ -54,7 +54,7 @@ class _DrawingCanvasState extends ConsumerState { Offset _globalToCanvas(Offset global) { final canvasBox = - _canvasPainterKey.currentContext!.findRenderObject() as RenderBox; + _canvasPainterKey.currentContext!.findRenderObject() as RenderBox; return canvasBox.globalToLocal(global); } @@ -106,7 +106,7 @@ class _DrawingCanvasState extends ConsumerState { Widget build(BuildContext context) { ref.listen( WorkspaceState.provider.select((state) => state.isFullscreen), - (wasFullscreen, isFullscreen) { + (wasFullscreen, isFullscreen) { _resetCanvasScale(fitToScreen: isFullscreen); }, ); @@ -121,19 +121,19 @@ class _DrawingCanvasState extends ConsumerState { boundaryMargin: const EdgeInsets.all(double.infinity), interactionEndFrictionCoefficient: double.minPositive, panEnabled: - ref.watch(toolBoxStateProvider).currentTool.type == ToolType.HAND, + ref.watch(toolBoxStateProvider).currentTool.type == ToolType.HAND, onInteractionStart: _onInteractionStart, onInteractionUpdate: _onInteractionUpdate, onInteractionEnd: _onInteractionEnd, child: Center( child: ref.watch(IDeviceService.sizeProvider).map( - data: (_) => FittedBox( - fit: BoxFit.contain, - child: CanvasPainter(key: _canvasPainterKey), - ), - error: (_) => Container(), - loading: (_) => Container(), - ), + data: (_) => FittedBox( + fit: BoxFit.contain, + child: CanvasPainter(key: _canvasPainterKey), + ), + error: (_) => Container(), + loading: (_) => Container(), + ), ), ), ); diff --git a/packages/features/workspace_screen/lib/src/components/top_bar/overflow_menu.dart b/packages/features/workspace_screen/lib/src/components/top_bar/overflow_menu.dart index 238c4863..f72cdbf0 100644 --- a/packages/features/workspace_screen/lib/src/components/top_bar/overflow_menu.dart +++ b/packages/features/workspace_screen/lib/src/components/top_bar/overflow_menu.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; +import 'package:command/command_providers.dart'; import 'package:component_library/component_library.dart'; import 'package:database/database.dart'; import 'package:flutter/material.dart'; @@ -7,13 +9,18 @@ import 'package:l10n/l10n.dart'; import 'package:oxidized/oxidized.dart'; import 'package:toast/toast.dart'; import 'package:workspace_screen/workspace_screen.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; + enum OverflowMenuOption { fullscreen, saveImage, saveProject, loadImage, - newImage; + newImage, + share; String localizedLabel(BuildContext context) { final localizations = AppLocalizations.of(context); @@ -28,6 +35,8 @@ enum OverflowMenuOption { return localizations.newImage; case OverflowMenuOption.saveProject: return localizations.saveProject; + case OverflowMenuOption.share: + return localizations.share; } } } @@ -48,11 +57,11 @@ class _OverflowMenuState extends ConsumerState { onSelected: _handleSelectedOption, itemBuilder: (BuildContext context) => OverflowMenuOption.values .map((option) => PopupMenuItem( - value: option, - child: Text( - option.localizedLabel(context), - style: TextThemes.menuItem, - ))) + value: option, + child: Text( + option.localizedLabel(context), + style: TextThemes.menuItem, + ))) .toList(), ); } @@ -75,6 +84,9 @@ class _OverflowMenuState extends ConsumerState { case OverflowMenuOption.newImage: ioHandler.newImage(context, this); break; + case OverflowMenuOption.share: + _shareContent(context); // Pass context to share content + break; } } @@ -114,7 +126,7 @@ class _OverflowMenuState extends ConsumerState { final fileService = ref.watch(IFileService.provider); final fileName = '${imageData.name}.${imageData.format.extension}'; final fileExists = - await fileService.checkIfFileExistsInApplicationDirectory(fileName); + await fileService.checkIfFileExistsInApplicationDirectory(fileName); if (fileExists) { final overWriteCanceled = await _showOverwriteDialog(); @@ -132,6 +144,66 @@ class _OverflowMenuState extends ConsumerState { return true; } + Future _shareContent(BuildContext context) async { // Added method + try { + final img = await _captureCanvasImage(context); // Use _captureCanvasImage + final byteData = await img.toByteData(format: ui.ImageByteFormat.png); + final pngBytes = byteData?.buffer.asUint8List(); + + if (pngBytes != null) { + final tempDir = await getTemporaryDirectory(); + final file = await File('${tempDir.path}/shared_image.png').create(); + await file.writeAsBytes(pngBytes); + + Share.shareXFiles([XFile(file.path)]); // Updated method + } else { + Toast.show( + 'Failed to capture image from canvas.', + duration: Toast.lengthShort, + gravity: Toast.bottom, + ); + } + } catch (e) { + Toast.show( + 'Error sharing content: $e', + duration: Toast.lengthShort, + gravity: Toast.bottom, + ); + } + } + + Future _captureCanvasImage(BuildContext context) async { // Added method to capture canvas image + final canvasState = ref.read(canvasStateProvider); + final commands = ref.read(commandManagerProvider); + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + final size = canvasState.size; + if (canvasState.backgroundImage != null) { + paintImage( + canvas: canvas, + rect: Offset.zero & size, + image: canvasState.backgroundImage!, + fit: BoxFit.cover, + ); + } + + if (canvasState.cachedImage != null) { + paintImage( + canvas: canvas, + rect: Offset.zero & size, + image: canvasState.cachedImage!, + fit: BoxFit.cover, + ); + } + + commands.executeAllCommands(canvas); + + final picture = recorder.endRecording(); + final img = await picture.toImage(size.width.toInt(), size.height.toInt()); + return img; + } + Future _saveProject() async { final imageData = await showSaveImageDialog(context, true); @@ -151,7 +223,7 @@ class _OverflowMenuState extends ConsumerState { final savedProject = await ioHandler.saveProject(catrobatImageData); if (savedProject != null) { String? imagePreview = - await ioHandler.getPreviewPath(catrobatImageData); + await ioHandler.getPreviewPath(catrobatImageData); Project projectNew = Project( name: catrobatImageData.name, path: savedProject.path, diff --git a/packages/features/workspace_screen/pubspec.yaml b/packages/features/workspace_screen/pubspec.yaml index a9f876a7..4345e918 100644 --- a/packages/features/workspace_screen/pubspec.yaml +++ b/packages/features/workspace_screen/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: toast: ^0.3.0 image: ^3.2.0 oxidized: ^5.2.0 + share_plus: ^7.2.2 + path_provider: ^2.1.3 # Internal packages component_library: diff --git a/packages/features/workspace_screen/test/widget/workspace_screen_test.dart b/packages/features/workspace_screen/test/widget/workspace_screen_test.dart index bd05bca4..b666b593 100644 --- a/packages/features/workspace_screen/test/widget/workspace_screen_test.dart +++ b/packages/features/workspace_screen/test/widget/workspace_screen_test.dart @@ -47,6 +47,25 @@ void main() { expect(overflowMenuButtonFinder, findsOneWidget); }); + testWidgets('Tapping share option maintains UI stability', + (WidgetTester tester) async { + await tester.pumpWidget(sut); + + await tester.tap(find.byIcon(Icons.more_vert)); + await tester.pumpAndSettle(); + + final initialWidgetTree = tester.widgetList(find.byType(Widget)).toString(); + + final shareOptionFinder = find.text('Share'); + await tester.ensureVisible(shareOptionFinder); + await tester.tap(shareOptionFinder); + await tester.pumpAndSettle(); + + final updatedWidgetTree = tester.widgetList(find.byType(Widget)).toString(); + + expect(updatedWidgetTree, initialWidgetTree); + }); + group('Fullscreen functionality', () { late WorkspaceState testWorkspaceState; late FakeCommandManager fakeCommandManager; diff --git a/packages/l10n/lib/src/l10n/app_localizations.dart b/packages/l10n/lib/src/l10n/app_localizations.dart index 86fb2fbb..7bd61344 100644 --- a/packages/l10n/lib/src/l10n/app_localizations.dart +++ b/packages/l10n/lib/src/l10n/app_localizations.dart @@ -145,6 +145,8 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Layers'** String get layers; + + String get share; } class _AppLocalizationsDelegate diff --git a/packages/l10n/lib/src/l10n/app_localizations_en.dart b/packages/l10n/lib/src/l10n/app_localizations_en.dart index 3d07e5cc..5f4be1c0 100644 --- a/packages/l10n/lib/src/l10n/app_localizations_en.dart +++ b/packages/l10n/lib/src/l10n/app_localizations_en.dart @@ -19,6 +19,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get saveProject => 'Save project'; + @override + String get share => 'Share'; + @override String get tools => 'Tools'; diff --git a/pubspec.lock b/pubspec.lock index 23ee001e..ea22a71c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1085,6 +1085,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.dev" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + url: "https://pub.dev" + source: hosted + version: "3.3.1" shared_preferences: dependency: "direct main" description: