Skip to content

Commit

Permalink
feat: allow sharing workouts (closes #118)
Browse files Browse the repository at this point in the history
  • Loading branch information
blockbasti committed May 13, 2022
1 parent 998f625 commit 4cb9f9d
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 184 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 31
compileSdkVersion 32

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
Expand All @@ -41,7 +41,7 @@ android {
defaultConfig {
applicationId "com.blockbasti.justanotherworkouttimer"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 32
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.6.21'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.1.0'
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down
4 changes: 2 additions & 2 deletions android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionSha256Sum=c9490e938b221daf0094982288e4038deed954a3f12fb54cbf270ddf4e37d879
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82
35 changes: 29 additions & 6 deletions lib/home_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

import 'generated/l10n.dart';
import 'settings_page.dart';
Expand Down Expand Up @@ -95,12 +96,15 @@ class HomePageState extends State<HomePage> {
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ReorderableDragStartListener(index: workout.position, child: const Icon(Icons.drag_handle)),
child: ReorderableDragStartListener(
index: workout.position, child: const Icon(Icons.drag_handle)),
),
Expanded(
child: ListTile(
title: Text(workout.title),
subtitle: Text(S.of(context).durationWithTime(Utils.formatSeconds(workout.duration))),
subtitle: Text(S
.of(context)
.durationWithTime(Utils.formatSeconds(workout.duration))),
),
),
IconButton(
Expand All @@ -110,7 +114,8 @@ class HomePageState extends State<HomePage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BuilderPage(workout: workout, newWorkout: false),
builder: (context) =>
BuilderPage(workout: workout, newWorkout: false),
),
).then((value) => _loadWorkouts());
},
Expand All @@ -133,10 +138,11 @@ class HomePageState extends State<HomePage> {
_showDeleteDialog(context, workout);
}),
IconButton(
tooltip: S.of(context).shareWorkout,
onPressed: () {
exportWorkout(workout.title);
shareWorkout(workout.title);
},
icon: const Icon(Icons.save_alt))
icon: const Icon(Icons.share)),
],
));

Expand All @@ -145,10 +151,27 @@ class HomePageState extends State<HomePage> {
appBar: AppBar(
title: Text(S.of(context).workouts),
actions: [
IconButton(
onPressed: () async {
var count = await importFile(false);
_loadWorkouts();
if (!mounted) return;
Fluttertoast.showToast(
msg: S.of(context).importedCount(count),
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER);
},
icon: const Icon(Icons.file_download),
tooltip: S.of(context).import,
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsPage())).then((value) => _loadWorkouts());
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()))
.then((value) => _loadWorkouts());
})
],
),
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"deleteExercise": "Delete exercise",
"deleteSet": "Delete set",
"deleteWorkout": "Delete workout",
"shareWorkout": "Share workout",
"duplicate": "Duplicate",
"durationLeft": "{timeLeft} of {timeTotal} left",
"durationWithTime": "Duration: {formattedTime}",
Expand Down
59 changes: 39 additions & 20 deletions lib/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ class SettingsPageState extends State<SettingsPage> {
PrefTitle(
title: Text(
S.of(context).general,
/* style: TextStyle(color: Colors.blue), */
),
),
PrefDropdown(
title: Text(S.of(context).language),
items: Languages.languages.map((lang) => DropdownMenuItem(value: lang.localeCode, child: Text(lang.displayName))).toList(),
items: Languages.languages
.map((lang) => DropdownMenuItem(
value: lang.localeCode, child: Text(lang.displayName)))
.toList(),
onChange: (String value) {
var lang = Languages.fromLocaleCode(value);
setState(() {
Expand All @@ -64,21 +66,28 @@ class SettingsPageState extends State<SettingsPage> {
Phoenix.rebirth(context);
},
items: [
DropdownMenuItem(value: 'dark', child: Text(S.of(context).theme_dark)),
DropdownMenuItem(value: 'light', child: Text(S.of(context).theme_light)),
DropdownMenuItem(value: 'system', child: Text(S.of(context).theme_system)),
DropdownMenuItem(
value: 'dark', child: Text(S.of(context).theme_dark)),
DropdownMenuItem(
value: 'light', child: Text(S.of(context).theme_light)),
DropdownMenuItem(
value: 'system', child: Text(S.of(context).theme_system)),
]),
PrefSwitch(
title: Text(S.of(context).keepScreenAwake),
pref: 'wakelock',
),
PrefSwitch(title: Text(S.of(context).settingHalfway), pref: 'halftime'),
PrefSwitch(title: Text(S.of(context).playTickEverySecond), pref: 'ticks'),
PrefSwitch(
title: Text(S.of(context).expanded_setlist), subtitle: Text(S.of(context).expanded_setlist_info), pref: 'expanded_setlist'),
title: Text(S.of(context).settingHalfway), pref: 'halftime'),
PrefSwitch(
title: Text(S.of(context).playTickEverySecond), pref: 'ticks'),
PrefSwitch(
title: Text(S.of(context).expanded_setlist),
subtitle: Text(S.of(context).expanded_setlist_info),
pref: 'expanded_setlist'),
PrefTitle(
title: Text(
S.of(context).backup, /* style: TextStyle(color: Colors.blue) */
S.of(context).backup,
)),
PrefLabel(
title: Text(S.of(context).export),
Expand All @@ -87,13 +96,15 @@ class SettingsPageState extends State<SettingsPage> {
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))
importFile(true).then((value) => Fluttertoast.showToast(
msg: S.of(context).importedCount(value),
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER))
},
),
PrefTitle(
title: Text(
S.of(context).soundOutput, /* style: TextStyle(color: Colors.blue) */
S.of(context).soundOutput,
)),
PrefRadio(
title: Text(S.of(context).noSound),
Expand Down Expand Up @@ -128,24 +139,30 @@ class SettingsPageState extends State<SettingsPage> {
),
PrefTitle(
title: Text(
S.of(context).tts, /* style: TextStyle(color: Colors.blue) */
S.of(context).tts,
),
),
PrefDropdown(
title: Text(S.of(context).ttsVoice),
pref: 'tts_voice',
subtitle: Text(S.of(context).ttsVoiceDesc),
items: TTSHelper.voices
.where((voice) => voice.locale == Prefs.getString('tts_lang', 'en-US'))
.where((voice) =>
voice.locale == Prefs.getString('tts_lang', 'en-US'))
.toList()
.asMap()
.entries
.map((voice) =>
DropdownMenuItem(value: voice.value.name, child: Text('${S.of(context).voice} ${voice.key + 1} (${voice.value.name})')))
.map((voice) => DropdownMenuItem(
value: voice.value.name,
child: Text(
'${S.of(context).voice} ${voice.key + 1} (${voice.value.name})')))
.toList(),
disabled: !TTSHelper.available,
onChange: (String value) {
TTSHelper.flutterTts.setVoice({"name": value, "locale": Prefs.getString('tts_lang', 'en-US')});
TTSHelper.flutterTts.setVoice({
"name": value,
"locale": Prefs.getString('tts_lang', 'en-US')
});
},
),
PrefSwitch(
Expand All @@ -156,13 +173,14 @@ class SettingsPageState extends State<SettingsPage> {
),
PrefTitle(
title: Text(
S.of(context).licenses, /* style: TextStyle(color: Colors.blue) */
S.of(context).licenses,
)),
PrefLabel(
title: Text(S.of(context).viewOnGithub),
subtitle: Text(S.of(context).reportIssuesOrRequestAFeature),
onTap: () {
launchUrlString('https://github.com/blockbasti/just_another_workout_timer');
launchUrlString(
'https://github.com/blockbasti/just_another_workout_timer');
},
),
PrefLabel(
Expand All @@ -177,7 +195,8 @@ class SettingsPageState extends State<SettingsPage> {
padding: const EdgeInsets.all(8),
child: Text(
S.of(context).title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
Padding(
Expand Down
59 changes: 44 additions & 15 deletions lib/storage_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:typed_data';

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

import 'migrations.dart';
import 'utils.dart';
Expand All @@ -25,35 +26,58 @@ Future<void> exportWorkout(String title) async {
var workout = await loadWorkout(title: title);
var backup = Backup(workouts: [workout]);
final params = SaveFileDialogParams(
data: Uint8List.fromList(jsonEncode(backup.toJson()).codeUnits), fileName: '${Utils.removeSpecialChar(title)}.json');
data: Uint8List.fromList(jsonEncode(backup.toJson()).codeUnits),
fileName: '${Utils.removeSpecialChar(title)}.json');
await FlutterFileDialog.saveFile(params: params);
}

Future<void> shareWorkout(String title) async {
final path = await localPath;
Share.shareFiles(['$path/workouts/${Utils.removeSpecialChar(title)}.json'],
text: title);
}

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

Future<int> importBackup() async {
const params = OpenFileDialogParams(dialogType: OpenFileDialogType.document, fileExtensionsFilter: ['json'], allowEditing: false);
final filePath = await FlutterFileDialog.pickFile(params: params);
Future<String?> pickFile() async {
const params = OpenFileDialogParams(
dialogType: OpenFileDialogType.document,
fileExtensionsFilter: ['json'],
allowEditing: false);
return FlutterFileDialog.pickFile(params: params);
}

Future<int> importFile(bool fromBackup) async {
String? filePath = await pickFile();
if (filePath != null && filePath.isNotEmpty) {
String backup;
String content;
var file = File(filePath);
try {
backup = await file.readAsString();
content = await file.readAsString();
} on FileSystemException {
// It might happen that encoding of files gets corrupted somehow.
// Therefore loading is tried again with 'allowMalformed' flag.
var bytes = await file.readAsBytes();
backup = utf8.decode(bytes, allowMalformed: true);
content = utf8.decode(bytes, allowMalformed: true);
// TODO: What to do here? Log error? Show warning?
}
var workouts = Backup.fromJson(jsonDecode(backup)).workouts;
workouts.forEach((w) => writeWorkout(w, fixDuplicates: true));
await Migrations.runMigrations();
return Future.value(workouts.length);

if (fromBackup) {
var workouts = Backup.fromJson(jsonDecode(content)).workouts;
workouts.forEach((w) => writeWorkout(w, fixDuplicates: true));
await Migrations.runMigrations();
return Future.value(workouts.length);
} else {
var workout = Workout.fromJson(jsonDecode(content));
writeWorkout(workout, fixDuplicates: true);
return Future.value(1);
}
} else {
return Future.value(0);
}
Expand Down Expand Up @@ -109,14 +133,19 @@ Future<void> createBackup() async {
Utils.copyDirectory(dir, dirbak);
var backup = Backup(workouts: await getAllWorkouts());
var backupfile = File('${dirbak.path}/backup.json');
backupfile.writeAsBytesSync(Uint8List.fromList(jsonEncode(backup.toJson()).codeUnits));
backupfile.writeAsBytesSync(
Uint8List.fromList(jsonEncode(backup.toJson()).codeUnits));
}

Future<List<Workout>> getAllWorkouts() async {
final path = await localPath;

var dir = Directory('$path/workouts');
var titles = dir.listSync().map((e) => e.path.split("/").last.split(".").first).toList();
var list = (await Future.wait(titles.map((t) async => await loadWorkout(title: t))));
var titles = dir
.listSync()
.map((e) => e.path.split("/").last.split(".").first)
.toList();
var list =
(await Future.wait(titles.map((t) async => await loadWorkout(title: t))));
return Utils.sortWorkouts(list);
}
Loading

0 comments on commit 4cb9f9d

Please sign in to comment.