From b10370e4fc0905b54ff081703f2f12d453799523 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:40:10 +0700 Subject: [PATCH] refactor(share): share the widget capture helper The lyrics card and the rewind slide both rendered a RepaintBoundary to a temp PNG. captureBoundaryToPngFile does that; pixel ratio, file name, frame waiting and share parameters stay with each caller. --- lib/screens/listening_rewind_screen.dart | 6 ++--- lib/screens/listening_rewind_story.dart | 17 +++++--------- lib/screens/lyrics_share_screen.dart | 22 +++++++----------- lib/utils/widget_capture.dart | 29 ++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 lib/utils/widget_capture.dart diff --git a/lib/screens/listening_rewind_screen.dart b/lib/screens/listening_rewind_screen.dart index ea0e078d..12de3ca0 100644 --- a/lib/screens/listening_rewind_screen.dart +++ b/lib/screens/listening_rewind_screen.dart @@ -1,17 +1,14 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math' as math; -import 'dart:ui' show ImageByteFormat, ImageFilter; +import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show RenderRepaintBoundary; import 'package:flutter/semantics.dart' show CustomSemanticsAction; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:material_3_expressive/material_3_expressive.dart' show M3EShapeKind; -import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/providers/listening_rewind_provider.dart'; @@ -19,6 +16,7 @@ import 'package:spotiflac_android/services/listening_rewind.dart'; import 'package:spotiflac_android/theme/app_motion.dart'; import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/theme/mornye_theme.dart'; +import 'package:spotiflac_android/utils/widget_capture.dart'; import 'package:spotiflac_android/widgets/app_action_button.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_content_card.dart'; diff --git a/lib/screens/listening_rewind_story.dart b/lib/screens/listening_rewind_story.dart index ee00a4f1..d972b2e0 100644 --- a/lib/screens/listening_rewind_story.dart +++ b/lib/screens/listening_rewind_story.dart @@ -137,17 +137,12 @@ class _RewindStoryState extends State<_RewindStory> final pixelRatio = MediaQuery.devicePixelRatioOf(context); setState(() => _sharing = true); try { - final boundary = - _captureKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - if (boundary == null) return; - final image = await boundary.toImage(pixelRatio: pixelRatio); - final bytes = await image.toByteData(format: ImageByteFormat.png); - image.dispose(); - if (bytes == null) return; - final directory = await getTemporaryDirectory(); - final file = File('${directory.path}/spotiflac_rewind.png'); - await file.writeAsBytes(bytes.buffer.asUint8List(), flush: true); + final file = await captureBoundaryToPngFile( + _captureKey, + pixelRatio: pixelRatio, + fileName: 'spotiflac_rewind.png', + ); + if (file == null) return; await SharePlus.instance.share( ShareParams( files: [XFile(file.path, mimeType: 'image/png')], diff --git a/lib/screens/lyrics_share_screen.dart b/lib/screens/lyrics_share_screen.dart index ad3d28c4..8f32c49c 100644 --- a/lib/screens/lyrics_share_screen.dart +++ b/lib/screens/lyrics_share_screen.dart @@ -1,17 +1,15 @@ -import 'dart:io'; -import 'dart:ui' show ImageByteFormat, ImageFilter; +import 'dart:ui' show ImageFilter; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' show RenderRepaintBoundary; import 'package:material_3_expressive/material_3_expressive.dart' show M3EShapeKind; -import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart' show ShareParams, SharePlus, XFile; import 'package:spotiflac_android/l10n/l10n.dart'; import 'package:spotiflac_android/theme/app_tokens.dart'; import 'package:spotiflac_android/theme/cover_palette.dart'; import 'package:spotiflac_android/utils/lyrics_parser.dart'; +import 'package:spotiflac_android/utils/widget_capture.dart'; import 'package:spotiflac_android/widgets/app_icon.dart'; import 'package:spotiflac_android/widgets/app_icon_button.dart'; import 'package:spotiflac_android/widgets/app_shape.dart'; @@ -151,16 +149,12 @@ class _LyricsShareScreenState extends State { // Share is tapped. Capture the current card after that frame finishes. await WidgetsBinding.instance.endOfFrame; if (!mounted) return; - final boundary = - _cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; - if (boundary == null) throw StateError('Card is not laid out'); - final image = await boundary.toImage(pixelRatio: 3); - final bytes = await image.toByteData(format: ImageByteFormat.png); - image.dispose(); - if (bytes == null) throw StateError('Card could not be encoded'); - final directory = await getTemporaryDirectory(); - final file = File('${directory.path}/spotiflac_lyrics.png'); - await file.writeAsBytes(bytes.buffer.asUint8List(), flush: true); + final file = await captureBoundaryToPngFile( + _cardKey, + pixelRatio: 3, + fileName: 'spotiflac_lyrics.png', + ); + if (file == null) throw StateError('Card could not be captured'); await SharePlus.instance.share( ShareParams( files: [XFile(file.path, mimeType: 'image/png')], diff --git a/lib/utils/widget_capture.dart b/lib/utils/widget_capture.dart new file mode 100644 index 00000000..b96b3dbd --- /dev/null +++ b/lib/utils/widget_capture.dart @@ -0,0 +1,29 @@ +import 'dart:io'; +import 'dart:ui' show ImageByteFormat; + +import 'package:flutter/rendering.dart' show RenderRepaintBoundary; +import 'package:flutter/widgets.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Renders the [RepaintBoundary] behind [boundaryKey] to a PNG named +/// [fileName] in the temporary directory. +/// +/// Returns null when the boundary is not laid out or the image cannot be +/// encoded, so each caller decides how to report that. +Future captureBoundaryToPngFile( + GlobalKey boundaryKey, { + required double pixelRatio, + required String fileName, +}) async { + final boundary = + boundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + if (boundary == null) return null; + final image = await boundary.toImage(pixelRatio: pixelRatio); + final bytes = await image.toByteData(format: ImageByteFormat.png); + image.dispose(); + if (bytes == null) return null; + final directory = await getTemporaryDirectory(); + final file = File('${directory.path}/$fileName'); + await file.writeAsBytes(bytes.buffer.asUint8List(), flush: true); + return file; +}