Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions lib/providers/app_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show SystemNavigator;
import 'package:flutter/widgets.dart'
show WidgetsBinding, WidgetsBindingObserver, AppLifecycleState;
show WidgetsBinding, WidgetsBindingObserver, AppLifecycleState, Rect;
import 'package:geolocator/geolocator.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:app_links/app_links.dart';
Expand Down Expand Up @@ -12035,12 +12035,15 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
/// Share a debug log file
///
/// Uses the native share sheet to allow users to share logs via email, messaging, etc.
Future<void> shareDebugLog(File file) async {
/// [sharePositionOrigin] is the tapped button's global rect, which iPad needs
/// (see [shareOfflineSession]).
Future<void> shareDebugLog(File file, {Rect? sharePositionOrigin}) async {
try {
final result = await SharePlus.instance.share(
ShareParams(
files: [XFile(file.path)],
subject: 'MeshMapper Debug Log',
sharePositionOrigin: sharePositionOrigin,
),
);
debugLog('[DEBUG] Shared log: ${file.path}, status: ${result.status}');
Expand All @@ -12053,7 +12056,12 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
/// to Files, Drive, email, …) — the mobile equivalent of the web JSON
/// download. Mirrors [shareDebugLog]; writes the same pretty JSON the web
/// path produces. Throws on failure so the caller can surface an error.
Future<void> shareOfflineSession(String filename) async {
///
/// [sharePositionOrigin] is the global rect of the tapped button. iPad
/// anchors the share popover there, and share_plus refuses to open the
/// sheet on iPad without one.
Future<void> shareOfflineSession(String filename,
{Rect? sharePositionOrigin}) async {
final data = _offlineSessionService.getSessionData(filename);
if (data == null) {
throw Exception('Session "$filename" not found');
Expand All @@ -12066,6 +12074,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
ShareParams(
files: [XFile(file.path)],
subject: 'MeshMapper Offline Session',
sharePositionOrigin: sharePositionOrigin,
),
);
debugLog('[OFFLINE] Shared session: $filename, status: ${result.status}');
Expand Down
13 changes: 9 additions & 4 deletions lib/screens/settings/about_settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import '../../providers/app_state_provider.dart';
import '../../services/debug_file_logger.dart';
import '../../utils/constants.dart';
import '../../utils/share_origin.dart';
import '../../widgets/app_toast.dart';
import '../../widgets/bug_report_dialog.dart';
import '../../widgets/upload_logs_dialog.dart';
Expand Down Expand Up @@ -292,10 +293,14 @@ class _AboutSettingsPageState extends State<AboutSettingsPage> {
_showLogViewer(context, appState, file),
tooltip: 'View',
),
IconButton(
icon: const Icon(Icons.share, size: 20),
onPressed: () => appState.shareDebugLog(file),
tooltip: 'Share',
Builder(
builder: (buttonContext) => IconButton(
icon: const Icon(Icons.share, size: 20),
onPressed: () => appState.shareDebugLog(file,
sharePositionOrigin:
globalRectOf(buttonContext)),
tooltip: 'Share',
),
),
],
),
Expand Down
30 changes: 19 additions & 11 deletions lib/screens/settings/data_settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import '../../utils/web_file_helpers_stub.dart'

import '../../providers/app_state_provider.dart';
import '../../services/offline_session_service.dart';
import '../../utils/debug_logger_io.dart';
import '../../utils/share_origin.dart';
import 'settings_section_card.dart';

/// Settings folder: Data.
Expand Down Expand Up @@ -83,8 +85,8 @@ class DataSettingsPage extends StatelessWidget {
context, appState, session.filename),
onDelete: () => _confirmDeleteOfflineSession(
context, appState, session.filename),
onDownload: () => _downloadOfflineSession(
context, appState, session.filename),
onDownload: (origin) => _downloadOfflineSession(
context, appState, session.filename, origin),
)),
]),
],
Expand Down Expand Up @@ -278,8 +280,8 @@ class DataSettingsPage extends StatelessWidget {
);
}

Future<void> _downloadOfflineSession(
BuildContext context, AppStateProvider appState, String filename) async {
Future<void> _downloadOfflineSession(BuildContext context,
AppStateProvider appState, String filename, Rect? origin) async {
try {
final sessionData =
appState.offlineSessionService.getSessionData(filename);
Expand Down Expand Up @@ -317,9 +319,11 @@ class DataSettingsPage extends StatelessWidget {
} else {
// Mobile: write the JSON to a temp file and open the native share
// sheet (Save to Files, Drive, email, …) — mirrors the debug-log share.
await appState.shareOfflineSession(filename);
await appState.shareOfflineSession(filename,
sharePositionOrigin: origin);
}
} catch (e) {
debugError('[OFFLINE] Failed to download session $filename: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
Expand All @@ -338,7 +342,9 @@ class _OfflineSessionTile extends StatelessWidget {
final bool uploadEnabled;
final VoidCallback onUpload;
final VoidCallback onDelete;
final VoidCallback onDownload;

/// Called with the download button's global rect (the iPad share anchor).
final ValueChanged<Rect?> onDownload;

const _OfflineSessionTile({
required this.session,
Expand Down Expand Up @@ -454,11 +460,13 @@ class _OfflineSessionTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
// Download button - always available
IconButton(
icon: const Icon(Icons.download),
onPressed: onDownload,
tooltip: 'Download JSON',
color: Colors.blue,
Builder(
builder: (buttonContext) => IconButton(
icon: const Icon(Icons.download),
onPressed: () => onDownload(globalRectOf(buttonContext)),
tooltip: 'Download JSON',
color: Colors.blue,
),
),
// Upload button - only when not uploaded
if (!isUploaded)
Expand Down
11 changes: 11 additions & 0 deletions lib/utils/share_origin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:flutter/widgets.dart';

/// Global rect of the widget behind [context], or null before layout.
///
/// Pass it as share_plus's `sharePositionOrigin`: iPad anchors the share
/// popover there and refuses to open the sheet without one.
Rect? globalRectOf(BuildContext context) {
final box = context.findRenderObject() as RenderBox?;
if (box == null || !box.hasSize) return null;
return box.localToGlobal(Offset.zero) & box.size;
}
Loading