Skip to content

Commit

Permalink
Re-added export functionality to example app
Browse files Browse the repository at this point in the history
Minor example app improvements
  • Loading branch information
JaffaKetchup committed Jan 6, 2025
1 parent d9bd045 commit 190b90b
Show file tree
Hide file tree
Showing 17 changed files with 406 additions and 382 deletions.
14 changes: 9 additions & 5 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'src/screens/export/export.dart';
import 'src/screens/import/import.dart';
import 'src/screens/initialisation_error/initialisation_error.dart';
import 'src/screens/main/main.dart';
import 'src/screens/main/secondary_view/contents/home/components/stores_list/state/export_selection_provider.dart';
import 'src/screens/store_editor/store_editor.dart';
import 'src/shared/misc/shared_preferences.dart';
import 'src/shared/state/download_configuration_provider.dart';
Expand Down Expand Up @@ -66,6 +66,14 @@ class _AppContainer extends StatelessWidget {
fullscreenDialog: true,
),
),
ExportPopup.route: (
std: null,
custom: (context, settings) => MaterialPageRoute(
builder: (context) => const ExportPopup(),
settings: settings,
fullscreenDialog: true,
),
),
};

@override
Expand Down Expand Up @@ -97,10 +105,6 @@ class _AppContainer extends StatelessWidget {
ChangeNotifierProvider(
create: (_) => GeneralProvider(),
),
ChangeNotifierProvider(
create: (_) => ExportSelectionProvider(),
lazy: true,
),
ChangeNotifierProvider(
create: (_) => RegionSelectionProvider(),
lazy: true,
Expand Down
329 changes: 329 additions & 0 deletions example/lib/src/screens/export/export.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';

class ExportPopup extends StatefulWidget {
const ExportPopup({super.key});

static const String route = '/export';

@override
State<ExportPopup> createState() => _ExportPopupState();
}

class _ExportPopupState extends State<ExportPopup> {
late final _inputController = TextEditingController();

final _availableStores = FMTCRoot.stats.storesAvailable;

final _selectedStores = <FMTCStore>{};

bool _isExporting = false;
bool _isVerifying = false;
bool _isInvalid = false;

@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Export Stores'),
),
body: FutureBuilder(
future: _availableStores,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator.adaptive(),
const SizedBox(height: 12),
Text(
'Loading stores',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
}

final stores = snapshot.requireData;

assert(
stores.isNotEmpty,
'This route should not be navigable if there are no stores',
);

final isMobilePlatform = Platform.isAndroid || Platform.isIOS;

final exportLoader = Padding(
key: const ValueKey('exportLoader'),
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Stack(
alignment: Alignment.center,
children: [
SizedBox.square(
dimension: 64,
child: CircularProgressIndicator.adaptive(),
),
Icon(Icons.send_and_archive, size: 32),
],
),
const SizedBox(width: 32),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Exporting selected stores...',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
const Text(
"Please don't close this dialog or leave the app.\n"
'The operation will continue if the dialog is '
"closed.\nWe'll let you know once we're done.",
),
],
),
),
],
),
);

final pathInput = Padding(
key: const ValueKey('pathInput'),
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
spacing: 8,
children: [
Expanded(
child: TextFormField(
controller: _inputController,
enabled: !_isVerifying,
decoration: InputDecoration(
suffixText: isMobilePlatform ? '.fmtc' : null,
filled: true,
label: isMobilePlatform
? const Text('Archive name')
: const Text('Archive path'),
errorText: _isInvalid ? 'Invalid name' : null,
),
onChanged: (_) => setState(() => _isInvalid = false),
),
),
if (!isMobilePlatform)
SizedBox(
height: 38,
child: AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.centerRight,
child: ValueListenableBuilder(
valueListenable: _inputController,
builder: (context, controller, _) {
if (controller.text.isEmpty) {
return FilledButton.icon(
onPressed: _launchPlatformPicker,
icon: const Icon(Icons.note_add),
label: const Text('Select file'),
);
} else {
return IconButton.filledTonal(
onPressed: _launchPlatformPicker,
icon: const Icon(Icons.note_add),
);
}
},
),
),
),
],
),
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) => Align(
alignment: Alignment.centerRight,
child: SizedBox(
height: 38,
width:
constraints.maxWidth > 500 ? 250 : double.infinity,
child: ValueListenableBuilder(
valueListenable: _inputController,
builder: (context, controller, _) {
final enabled = _selectedStores.isEmpty ||
_isVerifying ||
controller.text.isEmpty;

return FilledButton.icon(
onPressed: enabled ? null : _verifyAndExport,
icon: _isVerifying
? null
: const Icon(Icons.send_and_archive),
label: _isVerifying
? const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator.adaptive(
strokeWidth: 3,
),
)
: Text(
'Create archive & '
'${isMobilePlatform ? 'share' : 'save'}',
),
);
},
),
),
),
),
],
),
);

return Column(
children: [
Expanded(
child: Scrollbar(
child: ListView.builder(
itemCount: stores.length,
itemBuilder: (context, index) {
final store = stores[index];

return CheckboxListTile.adaptive(
title: Text(store.storeName),
value: _selectedStores.contains(store),
onChanged: _isVerifying
? null
: (value) {
if (value!) {
_selectedStores.add(store);
} else {
_selectedStores.remove(store);
}
setState(() {});
},
);
},
),
),
),
ColoredBox(
color: Theme.of(context).colorScheme.surfaceContainer,
child: SizedBox(
width: double.infinity,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (child, animation) => SlideTransition(
position: (animation.value == 1
? Tween(
begin: const Offset(-1, 0),
end: Offset.zero,
)
: Tween(
begin: const Offset(1, 0),
end: Offset.zero,
))
.animate(animation),
child: child,
),
child: _isExporting ? exportLoader : pathInput,
),
),
),
],
);
},
),
);

Future<void> _launchPlatformPicker() async {
final filePath = await FilePicker.platform.saveFile(
dialogTitle: 'Export Stores',
fileName: 'export.fmtc',
type: FileType.custom,
allowedExtensions: ['fmtc'],
);
if (filePath == null) return;
_inputController.text = filePath;
setState(() => _isInvalid = false);
}

Future<void> _verifyAndExport() async {
void errorOut() {
if (!mounted) return;
setState(() {
_isVerifying = false;
_isInvalid = true;
});
}

setState(() => _isVerifying = true);

late final String path;

if (Platform.isAndroid || Platform.isIOS) {
final tempDir =
p.join((await getTemporaryDirectory()).absolute.path, 'fmtc_export');
path = p.join(tempDir, '${_inputController.text}.fmtc.tmp');
} else {
path = _inputController.text;

late final FileSystemEntityType selectedType;
try {
selectedType = await FileSystemEntity.type(path);
} on FileSystemException {
return errorOut();
}
if (selectedType != FileSystemEntityType.notFound &&
selectedType != FileSystemEntityType.file) {
return errorOut();
}
}

final file = File(path);
try {
await file.create();
await file.delete();
} on FileSystemException {
return errorOut();
}

if (!mounted) return;
setState(() => _isExporting = true);

final stopwatch = Stopwatch()..start();

final tilesCount = await FMTCRoot.external(pathToArchive: path).export(
storeNames:
_selectedStores.map((s) => s.storeName).toList(growable: false),
);

stopwatch.stop();

if (Platform.isAndroid || Platform.isIOS) {
await Share.shareXFiles([XFile(path)]);
await File(path).delete(recursive: true);
}

if (!mounted) return;
Navigator.of(context).pop();
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(
content: Text('Exported $tilesCount tiles in ${stopwatch.elapsed}'),
),
);
}
}
1 change: 0 additions & 1 deletion example/lib/src/screens/import/import.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ class _ImportPopupState extends State<ImportPopup> {
appBar: AppBar(
title: const Text('Import Archive'),
automaticallyImplyLeading: stage != 3,
elevation: 1,
),
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
Expand Down
2 changes: 1 addition & 1 deletion example/lib/src/screens/import/stages/selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class _ImportSelectionStageState extends State<ImportSelectionStage> {
children: [
Icon(icon),
const SizedBox(width: 12),
Text(text),
Expanded(child: Text(text)),
],
),
);
Expand Down
Loading

0 comments on commit 190b90b

Please sign in to comment.