Skip to content

Commit

Permalink
feat: importing and exporting of workouts (closes #49) (#74)
Browse files Browse the repository at this point in the history
* functionality for importing and exporting workouts (#49)

* mark duplicates when importing a backup

* show message after import
  • Loading branch information
blockbasti authored Jun 29, 2021
1 parent 6f85da6 commit 80f8f6b
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 10 deletions.
7 changes: 6 additions & 1 deletion lib/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,12 @@ class _HomePageState extends State<HomePage> {
tooltip: S.of(context).deleteWorkout,
onPressed: () {
_showDeleteDialog(context, workout);
})
}),
IconButton(
onPressed: () {
exportWorkout(workout.title);
},
icon: Icon(Icons.save_alt))
],
));

Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/intl_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"language": "Sprache",
"general": "Allgemein",
"keepScreenAwake": "Bildschirm angeschaltet lassen",
"backup": "Sicherung",
"export": "Alle Workouts sichern",
"import": "Sicherung laden",
"importedCount": "{count} Workouts importiert",
"soundOutput": "Tonausgabe",
"noSound": "kein Ton",
"noSoundDesc": "Ton stummschalten",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"general": "General",
"language": "Language",
"keepScreenAwake": "Keep screen awake",
"backup": "Backup",
"export": "Export all workouts",
"import": "Import a backup",
"importedCount": "Imported {count} workouts",
"soundOutput": "Sound output",
"noSound": "No sound effects",
"noSoundDesc": "Mute all sound output",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"general": "Général",
"language": "Langue",
"keepScreenAwake": "Garder l'écran allumé",
"backup": "Backup",
"export": "Export all workouts",
"import": "Import a backup",
"importedCount": "Imported {count} workouts",
"soundOutput": "Retour audio",
"noSound": "Aucun effet sonore",
"noSoundDesc": "Couper tous les sons",
Expand Down
3 changes: 3 additions & 0 deletions lib/l10n/intl_it.arb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"language": "Lingua",
"general": "Generale",
"keepScreenAwake": "Mantieni schermo sempre acceso",
"backup": "Backup",
"export": "Export all workouts",
"import": "Import a backup",
"soundOutput": "Uscita suoni",
"noSound": "Nessun suono",
"noSoundDesc": "Non sarà riprodotto alcun suono",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/intl_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"general": "Общее",
"language": "Язык",
"keepScreenAwake": "Предотвратить выключение экрана",
"backup": "Backup",
"export": "Export all workouts",
"import": "Import a backup",
"importedCount": "Imported {count} workouts",
"soundOutput": "Звуки",
"noSound": "Без звуковых эффектов",
"noSoundDesc": "Выключить все звуки",
Expand Down
16 changes: 16 additions & 0 deletions lib/settings_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:just_another_workout_timer/storage_helper.dart';
import 'package:pref/pref.dart';
import 'package:url_launcher/url_launcher.dart';

Expand Down Expand Up @@ -60,6 +62,20 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(S.of(context).settingHalfway), pref: 'halftime'),
PrefSwitch(
title: Text(S.of(context).playTickEverySecond), pref: 'ticks'),
PrefTitle(title: Text(S.of(context).backup)),
PrefLabel(
title: Text(S.of(context).export),
onTap: exportAllWorkouts,
),
PrefLabel(
title: Text(S.of(context).import),
onTap: () => {
importBackup().then((value) => Fluttertoast.showToast(
msg: S.of(context).importedCount(value),
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER))
},
),
PrefTitle(title: Text(S.of(context).soundOutput)),
PrefRadio(
title: Text(S.of(context).noSound),
Expand Down
55 changes: 50 additions & 5 deletions lib/storage_helper.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:path_provider/path_provider.dart';

import 'utils.dart';
import 'workout.dart';

Future<String> get _localPath async {
final directory = await getExternalStorageDirectory();

await Directory('${directory!.path}/workouts').create();

return directory.path;
Expand All @@ -18,7 +19,50 @@ Future<File> _loadWorkoutFile(String title) async {
return File('$path/workouts/${Utils.removeSpecialChar(title)}.json');
}

void writeWorkout(Workout workout) async {
Future<void> exportWorkout(String title) async {
var workout = await loadWorkout(title: title);
var backup = Backup(workouts: [workout]);
final params = SaveFileDialogParams(
data: Uint8List.fromList(backup.toRawJson().codeUnits),
fileName: '${Utils.removeSpecialChar(title)}.json');
await FlutterFileDialog.saveFile(params: params);
}

Future<void> exportAllWorkouts() async {
var backup = Backup(workouts: await getAllWorkouts());
final params = SaveFileDialogParams(
data: Uint8List.fromList(backup.toRawJson().codeUnits),
fileName: 'Backup.json');
await FlutterFileDialog.saveFile(params: params);
}

Future<int> importBackup() async {
final params = OpenFileDialogParams(
dialogType: OpenFileDialogType.document,
fileExtensionsFilter: ['json'],
allowEditing: false);
final filePath = await FlutterFileDialog.pickFile(params: params);
if (filePath != null && filePath.isNotEmpty) {
var backup = await File(filePath).readAsString();
var workouts = Backup.fromRawJson(backup).workouts;
workouts.forEach((w) => writeWorkout(w, fixDuplicates: true));
return Future.value(workouts.length);
} else {
return Future.value(0);
}
}

Future<void> writeWorkout(Workout workout, {bool fixDuplicates = false}) async {
if (fixDuplicates) {
var counter = 2;
var newTitle = '${workout.title}';

while (await workoutExists(newTitle)) {
newTitle = '${workout.title}($counter)';
}
workout.title = newTitle;
}

final file = await _loadWorkoutFile(workout.title);

file.writeAsString(workout.toRawJson(), flush: true);
Expand All @@ -29,8 +73,8 @@ Future<bool> workoutExists(String title) async {
return file.exists();
}

Future<Workout> loadWorkout(String title) async {
final file = await _loadWorkoutFile(title);
Future<Workout> loadWorkout({String? title, File? workoutFile}) async {
final file = workoutFile ?? await _loadWorkoutFile(title!);
var contents = await file.readAsString();

return Workout.fromRawJson(contents);
Expand Down Expand Up @@ -62,5 +106,6 @@ Future<List<Workout>> getAllWorkouts() async {
.listSync()
.map((e) => e.path.split("/").last.split(".").first)
.toList();
return await Future.wait(titles.map((t) async => await loadWorkout(t)));
return await Future.wait(
titles.map((t) async => await loadWorkout(title: t)));
}
19 changes: 19 additions & 0 deletions lib/workout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,22 @@ class Exercise {
"duration": duration,
};
}

class Backup {
List<Workout> workouts;

Backup({required this.workouts});

factory Backup.fromRawJson(String str) => Backup.fromJson(json.decode(str));

String toRawJson() => json.encode(toJson());

factory Backup.fromJson(Map<String, dynamic> json) => Backup(
workouts: List<Workout>.from(
json["workouts"].map((x) => Workout.fromJson(x))),
);

Map<String, dynamic> toJson() => {
"workouts": List<dynamic>.from(workouts.map((x) => x.toJson())),
};
}
13 changes: 10 additions & 3 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ packages:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
version: "1.1.2"
file:
dependency: transitive
description:
Expand All @@ -132,6 +132,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_file_dialog:
dependency: "direct main"
description:
name: flutter_file_dialog
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -515,7 +522,7 @@ packages:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
url_launcher_windows:
dependency: transitive
description:
Expand Down Expand Up @@ -592,7 +599,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
version: "5.1.2"
yaml:
dependency: transitive
description:
Expand Down
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ description: Just Another Workout Timer
version: 1.6.0+20210311

environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.13.0 <3.0.0"

dependencies:
flutter:
sdk: flutter
flutter_tts:
path_provider:
flutter_file_dialog:
pref:
fluttertoast:
prefs:
Expand Down

0 comments on commit 80f8f6b

Please sign in to comment.