diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
new file mode 100644
index 00000000..fc1e445d
--- /dev/null
+++ b/.github/workflows/android-build.yml
@@ -0,0 +1,60 @@
+name: Android Build
+
+on:
+ pull_request:
+ branches:
+ - "*"
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions: write-all
+
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: "17"
+ cache: 'gradle'
+
+ - uses: subosito/flutter-action@v2
+ with:
+ flutter-version: "3.16.3"
+ channel: 'stable'
+ cache: true
+
+ - name: Create key.properties
+ run: |
+ echo keyPassword=\${{ secrets.KEY_PASSWORD }} > ./android/key.properties
+ echo storePassword=\${{ secrets.KEY_STORE_PASSWORD }} >> ./android/key.properties
+ echo keyAlias=\${{ secrets.KEY_ALIAS }} >> ./android/key.properties
+
+ - name: Load key
+ run: echo "${{ secrets.KEYSTORE_JKS_RELEASE }}" | base64 --decode > android/app/release-key.jks
+
+ - name: Turn off analytics
+ run: flutter config --no-analytics
+
+ - name: Pub Get Packages
+ run: flutter pub get
+
+ - name: Build arm APK
+ run: flutter build apk --release --split-per-abi --target-platform="android-arm"
+
+ - name: Build arm64 APK
+ run: flutter build apk --release --split-per-abi --target-platform="android-arm64"
+
+ - name: Build x64 APK
+ run: flutter build apk --release --split-per-abi --target-platform="android-x64"
+
+ - name: Save APKs to Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: APKs
+ path: build/app/outputs/flutter-apk/*.apk
+ retention-days: 3
diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml
index 32bc7e10..f899e8e8 100644
--- a/.github/workflows/android-release.yml
+++ b/.github/workflows/android-release.yml
@@ -1,38 +1,30 @@
name: Android Release
-# 1
on:
- # 2
push:
tags:
- '*'
- # 3
workflow_dispatch:
# 4
jobs:
- # 5
build:
- # 6
runs-on: ubuntu-latest
permissions: write-all
- # 7
steps:
- # 8
- uses: actions/checkout@v3
with:
fetch-depth: 0
- # 9
+
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: "17"
cache: 'gradle'
- # 10
+
- uses: subosito/flutter-action@v2
with:
- # 11
flutter-version: "3.16.3"
channel: 'stable'
cache: true
@@ -43,7 +35,6 @@ jobs:
echo storePassword=\${{ secrets.KEY_STORE_PASSWORD }} >> ./android/key.properties
echo keyAlias=\${{ secrets.KEY_ALIAS }} >> ./android/key.properties
-
- name: Load key
run: echo "${{ secrets.KEYSTORE_JKS_RELEASE }}" | base64 --decode > android/app/release-key.jks
@@ -73,21 +64,33 @@ jobs:
uses: mikepenz/release-changelog-builder-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ configurationJson: |
+ {
+ "template": "#{{CHANGELOG}}\n\n\nUncategorized
\n\n#{{UNCATEGORIZED}}\n ",
+ "categories": [
+ {
+ "title": "## ๐ Features",
+ "labels": ["feature"]
+ },
+ {
+ "enhancements": "## โจ Enhancements"
+ "labels": ["enhancement"]
+ },
+ {
+ "title": "## ๐ Fixes",
+ "labels": ["bug"]
+ },
+ {
+ "key": "tests",
+ "title": "## ๐งช Tests",
+ "labels": ["test"]
+ },
+ ],
+ }
- name: Create Github Release
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/bundle/release/*.aab,build/app/outputs/flutter-apk/*.apk"
body: ${{steps.github_release.outputs.changelog}}
- # tag: $GIT_TAG_NAME
- # token: ${{ secrets.PERSONAL_RELEASE_TOKEN }}
- #
- # - name: Save APPBUNDLE to Artifacts
- # uses: actions/upload-artifact@v2
- # with:
- # name: APPBUNDLE
- # path: build/app/outputs/bundle/release/*.aab
-
-
-
-
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 030c4714..0696401e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,10 +2,11 @@ name: Tests
on:
push:
- branches: [master]
+ branches:
+ - "*"
pull_request:
- branches: [master]
-
+ branches:
+ - "*"
jobs:
test:
name: Test
diff --git a/lib/alarm/logic/schedule_description.dart b/lib/alarm/logic/schedule_description.dart
index 3d34e0da..5345da61 100644
--- a/lib/alarm/logic/schedule_description.dart
+++ b/lib/alarm/logic/schedule_description.dart
@@ -1,3 +1,4 @@
+import 'package:clock_app/alarm/types/range_interval.dart';
import 'package:clock_app/common/data/weekdays.dart';
import 'package:clock_app/alarm/types/alarm.dart';
import 'package:clock_app/alarm/types/schedules/daily_alarm_schedule.dart';
@@ -43,11 +44,11 @@ String getAlarmScheduleDescription(Alarm alarm, String dateFormat) {
return 'Every ${weekdays.where((weekday) => alarmWeekdays.contains(weekday)).map((weekday) => weekday.displayName).join(', ')}';
case DatesAlarmSchedule:
List dates = alarm.dates;
- return 'On ${DateFormat(dateFormat).format(dates[0])}${dates.length > 1 ? ' and ${dates.length - 1} other${dates.length > 2 ? 's' : ''}' : ''}';
+ return 'On ${DateFormat(dateFormat).format(dates[0])}${dates.length > 1 ? ' and ${dates.length - 1} other date${dates.length > 2 ? 's' : ''} ' : ''}';
case RangeAlarmSchedule:
DateTime rangeStart = alarm.startDate;
DateTime rangeEnd = alarm.endDate;
- Duration interval = alarm.interval;
+ RangeInterval interval = alarm.interval;
String startString = DateFormat(dateFormat).format(rangeStart);
String endString = DateFormat(dateFormat).format(rangeEnd);
@@ -65,7 +66,7 @@ String getAlarmScheduleDescription(Alarm alarm, String dateFormat) {
}
}
- return '${interval.inDays == 1 ? "Daily" : "Weekly"} from $startString to $endString';
+ return '${interval == RangeInterval.daily ? "Daily" : "Weekly"} from $startString to $endString';
default:
return 'Not scheduled';
}
diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart
index 3a3fc838..af0664d5 100644
--- a/lib/alarm/types/alarm.dart
+++ b/lib/alarm/types/alarm.dart
@@ -1,6 +1,7 @@
import 'package:clock_app/alarm/logic/schedule_alarm.dart';
import 'package:clock_app/alarm/types/alarm_runner.dart';
import 'package:clock_app/alarm/types/alarm_task.dart';
+import 'package:clock_app/alarm/types/range_interval.dart';
import 'package:clock_app/alarm/types/schedules/alarm_schedule.dart';
import 'package:clock_app/alarm/types/schedules/daily_alarm_schedule.dart';
import 'package:clock_app/alarm/types/schedules/dates_alarm_schedule.dart';
@@ -261,8 +262,8 @@ class Alarm extends CustomizableListItem {
return (getSetting("Date Range") as DateTimeSetting).value[1];
}
- Duration get interval {
- return (getSetting("Interval") as SelectSetting).value;
+ RangeInterval get interval {
+ return (getSetting("Interval") as SelectSetting).value;
}
Alarm.fromJson(Json json)
diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart
index e43eb719..8f99677b 100644
--- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart
+++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart
@@ -44,6 +44,10 @@ class WeeklyAlarmSchedule extends AlarmSchedule {
WeekdaySchedule get nextWeekdaySchedule {
if (_weekdaySchedules.isEmpty) return WeekdaySchedule(0);
+ if (_weekdaySchedules.any((weeklySchedule) =>
+ weeklySchedule.alarmRunner.currentScheduleDateTime == null)) {
+ return _weekdaySchedules[0];
+ }
return _weekdaySchedules.reduce((a, b) => a
.alarmRunner.currentScheduleDateTime!
.isBefore(b.alarmRunner.currentScheduleDateTime!)
diff --git a/lib/app.dart b/lib/app.dart
index 4861fd65..cd9017a1 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -1,4 +1,5 @@
import 'package:clock_app/alarm/screens/alarm_notification_screen.dart';
+import 'package:clock_app/common/logic/card_decoration.dart';
import 'package:clock_app/navigation/data/route_observer.dart';
import 'package:clock_app/navigation/screens/nav_scaffold.dart';
import 'package:clock_app/navigation/types/routes.dart';
@@ -116,12 +117,7 @@ class OnBoardingPageState extends State {
borderRadius: BorderRadius.all(Radius.circular(25.0)),
),
),
- dotsContainerDecorator: ShapeDecoration(
- color: colorScheme.surface,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.all(Radius.circular(8.0)),
- ),
- ),
+ dotsContainerDecorator: getCardDecoration(context),
);
}
}
@@ -145,6 +141,11 @@ class App extends StatefulWidget {
_AppState state = context.findAncestorStateOfType<_AppState>()!;
state.setStyleTheme(styleTheme);
}
+
+ static void refreshTheme(BuildContext context) {
+ _AppState state = context.findAncestorStateOfType<_AppState>()!;
+ state.refreshTheme();
+ }
}
class _AppState extends State {
@@ -168,6 +169,11 @@ class _AppState extends State {
setStyleTheme(_styleSettings.getSetting("Style Theme").value);
}
+ refreshTheme() {
+ setColorScheme(_colorSettings.getSetting("Color Scheme").value);
+ setStyleTheme(_styleSettings.getSetting("Style Theme").value);
+ }
+
setColorScheme(ColorSchemeData? colorSchemeDataParam) {
ColorSchemeData colorSchemeData =
colorSchemeDataParam ?? _colorSettings.getSetting("Color Scheme").value;
diff --git a/lib/common/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart
new file mode 100644
index 00000000..df0857bb
--- /dev/null
+++ b/lib/common/logic/card_decoration.dart
@@ -0,0 +1,43 @@
+import 'package:clock_app/theme/types/theme_extension.dart';
+import 'package:flutter/material.dart';
+
+BoxDecoration getCardDecoration(BuildContext context,
+ {Color? color,
+ bool showLightBorder = false,
+ showShadow = true,
+ elevationMultiplier = 1,
+ blurStyle = BlurStyle.normal}) {
+ ThemeData theme = Theme.of(context);
+ ColorScheme colorScheme = theme.colorScheme;
+ ThemeStyleExtension? themeStyle = theme.extension();
+
+ return BoxDecoration(
+ border: showLightBorder
+ ? Border.all(
+ color: colorScheme.outline.withOpacity(0.2),
+ width: 0.5,
+ strokeAlign: BorderSide.strokeAlignInside,
+ )
+ : (themeStyle?.borderWidth != 0)
+ ? Border.all(
+ color: colorScheme.outline,
+ width: themeStyle?.borderWidth ?? 0.5,
+ strokeAlign: BorderSide.strokeAlignInside,
+ )
+ : null,
+ color: color ?? colorScheme.surface,
+ borderRadius:
+ (theme.cardTheme.shape as RoundedRectangleBorder).borderRadius,
+ boxShadow: [
+ if (showShadow && (themeStyle?.shadowOpacity ?? 0) > 0)
+ BoxShadow(
+ blurStyle: blurStyle,
+ color: colorScheme.shadow.withOpacity(themeStyle?.shadowOpacity ?? 1),
+ blurRadius: themeStyle?.shadowBlurRadius ?? 5,
+ spreadRadius: themeStyle?.shadowSpreadRadius ?? 0,
+ offset: Offset(
+ 0, (themeStyle?.shadowElevation ?? 1) * elevationMultiplier),
+ ),
+ ],
+ );
+}
diff --git a/lib/common/utils/date_time.dart b/lib/common/utils/date_time.dart
index a2bbd72e..1f502c2f 100644
--- a/lib/common/utils/date_time.dart
+++ b/lib/common/utils/date_time.dart
@@ -23,4 +23,6 @@ extension DateTimeUtils on DateTime {
month == tomorrow.month &&
day == tomorrow.day;
}
+
+ String toIso8601Date() => toIso8601String().substring(0, 10);
}
diff --git a/lib/common/widgets/card_container.dart b/lib/common/widgets/card_container.dart
index 6e2cc5c3..feeb1584 100644
--- a/lib/common/widgets/card_container.dart
+++ b/lib/common/widgets/card_container.dart
@@ -1,3 +1,4 @@
+import 'package:clock_app/common/logic/card_decoration.dart';
import 'package:clock_app/theme/types/theme_extension.dart';
import 'package:flutter/material.dart';
@@ -27,44 +28,17 @@ class CardContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
- ThemeData theme = Theme.of(context);
- ColorScheme colorScheme = theme.colorScheme;
- ThemeStyleExtension? themeStyle = theme.extension();
-
return Container(
alignment: alignment,
margin: margin ?? const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
- decoration: BoxDecoration(
- border: showLightBorder
- ? Border.all(
- color: colorScheme.outline.withOpacity(0.2),
- width: 0.5,
- strokeAlign: BorderSide.strokeAlignInside,
- )
- : (themeStyle?.borderWidth != 0)
- ? Border.all(
- color: colorScheme.outline,
- width: themeStyle?.borderWidth ?? 0.5,
- strokeAlign: BorderSide.strokeAlignInside,
- )
- : null,
- color: color ?? Theme.of(context).colorScheme.surface,
- borderRadius:
- (Theme.of(context).cardTheme.shape as RoundedRectangleBorder)
- .borderRadius,
- boxShadow: [
- if (showShadow && (themeStyle?.shadowOpacity ?? 0) > 0)
- BoxShadow(
- blurStyle: blurStyle,
- color: colorScheme.shadow
- .withOpacity(themeStyle?.shadowOpacity ?? 1),
- blurRadius: themeStyle?.shadowBlurRadius ?? 5,
- spreadRadius: themeStyle?.shadowSpreadRadius ?? 0,
- offset: Offset(
- 0, (themeStyle?.shadowElevation ?? 1) * elevationMultiplier),
- ),
- ],
+ decoration: getCardDecoration(
+ context,
+ color: color,
+ showLightBorder: showLightBorder,
+ showShadow: showShadow,
+ elevationMultiplier: elevationMultiplier,
+ blurStyle: blurStyle,
),
child: onTap == null
? child
diff --git a/lib/common/widgets/fields/date_picker_field.dart b/lib/common/widgets/fields/date_picker_field.dart
index 8e4822d6..1d8a7681 100644
--- a/lib/common/widgets/fields/date_picker_field.dart
+++ b/lib/common/widgets/fields/date_picker_field.dart
@@ -1,3 +1,4 @@
+import 'package:clock_app/common/widgets/card_container.dart';
import 'package:clock_app/common/widgets/fields/date_picker_bottom_sheet.dart';
import 'package:clock_app/settings/data/settings_schema.dart';
import 'package:clock_app/settings/types/setting.dart';
@@ -142,17 +143,18 @@ class DateChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
- return Chip(
- backgroundColor: colorScheme.onBackground.withOpacity(0.1),
- // labelPadding: EdgeInsets.zero,
- padding: EdgeInsets.zero,
-
- // side: ,
- label: Text(
- DateFormat(dateFormat).format(date),
- style: const TextStyle(fontSize: 10),
+ return CardContainer(
+ key: const Key("DateChip"),
+ color: colorScheme.primary,
+ margin: const EdgeInsets.all(0),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ DateFormat(dateFormat).format(date),
+ style: const TextStyle(fontSize: 10)
+ .copyWith(color: colorScheme.onPrimary),
+ ),
),
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}
}
diff --git a/lib/common/widgets/fields/select_field/option_cards/audio_option_card.dart b/lib/common/widgets/fields/select_field/option_cards/audio_option_card.dart
index 7dd20e29..c01dba79 100644
--- a/lib/common/widgets/fields/select_field/option_cards/audio_option_card.dart
+++ b/lib/common/widgets/fields/select_field/option_cards/audio_option_card.dart
@@ -66,17 +66,26 @@ class _SelectAudioOptionCardState extends State {
groupValue: widget.selectedIndex,
onChanged: (dynamic value) => widget.onSelect(widget.index),
),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(widget.choice.value.title,
- style: textTheme.headlineMedium),
- if (widget.choice.description.isNotEmpty)
- const SizedBox(height: 4.0),
- if (widget.choice.description.isNotEmpty)
- Text(widget.choice.description,
- style: textTheme.bodyMedium),
- ],
+ Expanded(
+ flex: 100,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ // Flutter doesn't allow per character overflow, so this is a workaround
+ widget.choice.value.title.replaceAll('', '\u{200B}'),
+ style: textTheme.headlineMedium,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ softWrap: false,
+ ),
+ if (widget.choice.description.isNotEmpty)
+ const SizedBox(height: 4.0),
+ if (widget.choice.description.isNotEmpty)
+ Text(widget.choice.description,
+ style: textTheme.bodyMedium),
+ ],
+ ),
),
const Spacer(),
IconButton(
diff --git a/lib/settings/data/settings_schema.dart b/lib/settings/data/settings_schema.dart
index 43e67c7b..a02ef420 100644
--- a/lib/settings/data/settings_schema.dart
+++ b/lib/settings/data/settings_schema.dart
@@ -1,3 +1,6 @@
+import 'dart:convert';
+import 'dart:io';
+
import 'package:app_settings/app_settings.dart';
import 'package:auto_start_flutter/auto_start_flutter.dart';
import 'package:clock_app/alarm/data/alarm_settings_schema.dart';
@@ -25,14 +28,18 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
+import 'package:pick_or_save/pick_or_save.dart';
SelectSettingOption _getDateSettingOption(String format) {
return SelectSettingOption(
"${DateFormat(format).format(DateTime.now())} ($format)", format);
}
+const int settingsSchemaVersion = 1;
+
SettingGroup appSettings = SettingGroup(
"Settings",
+ version: settingsSchemaVersion,
isSearchable: true,
[
SettingGroup(
@@ -43,13 +50,22 @@ SettingGroup appSettings = SettingGroup(
"Date Format",
() => [
_getDateSettingOption("dd/MM/yyyy"),
- _getDateSettingOption("dd/MM/yyyy"),
+ _getDateSettingOption("dd-MM-yyyy"),
_getDateSettingOption("d/M/yyyy"),
+ _getDateSettingOption("d-M-yyyy"),
_getDateSettingOption("MM/dd/yyyy"),
+ _getDateSettingOption("MM-dd-yyyy"),
_getDateSettingOption("M/d/yy"),
+ _getDateSettingOption("M-d-yy"),
_getDateSettingOption("M/d/yyyy"),
+ _getDateSettingOption("M-d-yyyy"),
+ _getDateSettingOption("yyyy/dd/MM"),
_getDateSettingOption("yyyy-dd-MM"),
- _getDateSettingOption("d-MMM-yyyy"),
+ _getDateSettingOption("yyyy/MM/dd"),
+ _getDateSettingOption("yyyy-MM-dd"),
+ // SelectSettingOption(DateTime.now().toIso8601Date(), "YYYY-MM-DD"),
+ _getDateSettingOption("d MMM yyyy"),
+ _getDateSettingOption("d MMMM yyyy"),
],
description: "How to display the dates",
),
@@ -92,7 +108,7 @@ SettingGroup appSettings = SettingGroup(
(context) async {
try {
//check auto-start availability.
- var test = await isAutoStartAvailable ?? false;
+ var test = (await isAutoStartAvailable) ?? false;
//if available then navigate to auto-start setting page.
if (test) {
await getAutoStartPermission();
@@ -120,7 +136,7 @@ SettingGroup appSettings = SettingGroup(
}
},
description:
- "Enable auto start to allow alarms to go off when the app is closed",
+ "Some devices require Auto Start to be enabled for alarms to ring while app is closed.",
)
]),
],
@@ -207,7 +223,7 @@ SettingGroup appSettings = SettingGroup(
],
),
],
- icon: FluxIcons.settings,
+ icon: Icons.palette_outlined,
description: "Set themes, colors and change layout",
),
SettingGroup(
@@ -297,9 +313,88 @@ SettingGroup appSettings = SettingGroup(
SettingGroup(
"Accessibility",
[SwitchSetting("Left Handed Mode", false)],
- icon: Icons.accessibility,
+ icon: Icons.accessibility_new_rounded,
showExpandedView: false,
),
+ SettingGroup(
+ "Backup",
+ description: "Export or Import your settings locally",
+ icon: Icons.restore_rounded,
+ [
+ SettingGroup(
+ "Settings",
+ [
+ SettingAction(
+ "Export",
+ (context) async {
+ saveBackupFile(
+ json.encode(appSettings.valueToJson()), "settings");
+ },
+ searchTags: ["settings", "export", "backup", "save"],
+ description: "Export settings to a local file",
+ ),
+ SettingAction(
+ "Import",
+ (context) async {
+ loadBackupFile(
+ (data) {
+ appSettings.loadValueFromJson(json.decode(data));
+ appSettings.callAllListeners();
+ App.refreshTheme(context);
+ },
+ );
+ },
+ searchTags: ["settings", "import", "backup", "load"],
+ description: "Import settings from a local file",
+ ),
+ ],
+ ),
+ // SettingGroup(
+ // "Alarms",
+ // [
+ // SettingAction(
+ // "Export",
+ // (context) async {
+ // saveBackupFile(
+ // json.encode(appSettings.valueToJson()), "alarms");
+ // },
+ // ),
+ // SettingAction(
+ // "Import",
+ // (context) async {
+ // loadBackupFile((data) {
+ // appSettings.loadValueFromJson(json.decode(data));
+ // appSettings.callAllListeners();
+ // App.refreshTheme(context);
+ // });
+ // },
+ // ),
+ // ],
+ // ),
+ // SettingGroup(
+ // "Timers",
+ // [
+ // SettingAction(
+ // "Export",
+ // (context) async {
+ // saveBackupFile(
+ // json.encode(appSettings.valueToJson()), "timers");
+ // },
+ // ),
+ // SettingAction(
+ // "Import",
+ // (context) async {
+ // loadBackupFile((data) {
+ // appSettings.loadValueFromJson(json.decode(data));
+ // appSettings.callAllListeners();
+ // App.refreshTheme(context);
+ // });
+ // },
+ // ),
+ // ],
+ // ),
+ ],
+ ),
SettingGroup(
"Developer Options",
[
@@ -312,9 +407,32 @@ SettingGroup appSettings = SettingGroup(
),
]),
],
- icon: Icons.code,
+ icon: Icons.code_rounded,
),
],
);
+saveBackupFile(String data, String label) async {
+ await PickOrSave().fileSaver(
+ params: FileSaverParams(
+ saveFiles: [
+ SaveFileInfo(
+ fileData: Uint8List.fromList(utf8.encode(data)),
+ fileName: "chrono_${label}_backup_${DateTime.now().toIso8601String()}",
+ )
+ ],
+ ));
+}
+
+loadBackupFile(Function(String) onSuccess) async {
+ List? result = await PickOrSave().filePicker(
+ params: FilePickerParams(
+ getCachedFilePath: true,
+ ),
+ );
+ if (result != null && result.isNotEmpty) {
+ File file = File(result[0]);
+ onSuccess(utf8.decode(file.readAsBytesSync()));
+ }
+}
// Settings appSettings = Settings(settingsItems);
diff --git a/lib/settings/screens/vendor_list_screen.dart b/lib/settings/screens/vendor_list_screen.dart
index 8bbac9f3..cca50db8 100644
--- a/lib/settings/screens/vendor_list_screen.dart
+++ b/lib/settings/screens/vendor_list_screen.dart
@@ -29,9 +29,13 @@ class _VendorListScreenState extends State {
// If the server did return a 200 OK response,
// then parse the JSON.
final json = jsonDecode(response.body);
- return (json["vendors"] as List)
+ final list = (json["vendors"] as List)
.map((json) => Vendor.fromJson(json))
.toList();
+ // Filter out duplicates
+ final names = {};
+ list.retainWhere((vendor) => names.add(vendor.name));
+ return list;
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
diff --git a/lib/settings/types/setting_group.dart b/lib/settings/types/setting_group.dart
index 659c385f..06e85cff 100644
--- a/lib/settings/types/setting_group.dart
+++ b/lib/settings/types/setting_group.dart
@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
class SettingGroup extends SettingItem {
+ final int? _version;
final IconData? _icon;
final List _summarySettings;
final bool? _showExpandedView;
@@ -33,6 +34,7 @@ class SettingGroup extends SettingItem {
SettingGroup(
String name,
this._settingItems, {
+ int? version,
IconData? icon,
List summarySettings = const [],
String description = "",
@@ -47,6 +49,7 @@ class SettingGroup extends SettingItem {
_settings = [],
_settingPageLinks = [],
_settingActions = [],
+ _version = version,
super(name, description, searchTags) {
for (SettingItem item in _settingItems) {
item.parent = this;
@@ -127,14 +130,36 @@ class SettingGroup extends SettingItem {
@override
dynamic valueToJson() {
Json json = {};
+ if (_version != null) json["version"] = _version;
for (var setting in _settingItems) {
json[setting.name] = setting.valueToJson();
}
return json;
}
+ void callAllListeners() {
+ for (var setting in settings) {
+ setting.callListeners(setting);
+ }
+ }
+
@override
void loadValueFromJson(dynamic value) {
+ if (_version != null && value["version"] != _version) {
+ //TODO: Add migration code
+
+ //In case of name change:
+ //value["New Name"] = value["Old Name"];
+ //OR
+ //value["Group 1"]["New Name"] = value["Group 1"]["Old Name"];
+ //value.remove("Old Name");
+
+ //Incase of addition
+ //value["New Setting"] = defaultValue;
+
+ //Incase of removal
+ //value.remove("Old Setting");
+ }
for (var setting in _settingItems) {
setting.loadValueFromJson(value[setting.name]);
}
diff --git a/pubspec.lock b/pubspec.lock
index 92544ea9..c33be071 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -631,10 +631,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
- sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
+ sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
url: "https://pub.dev"
source: hosted
- version: "2.1.3"
+ version: "2.1.7"
petitparser:
dependency: transitive
description:
@@ -643,6 +643,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.0"
+ pick_or_save:
+ dependency: "direct main"
+ description:
+ name: pick_or_save
+ sha256: "5e562e714e8486000b1144e580dfbd6db888a0b4dd02bf4c28501b244dd22fd3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.4"
platform:
dependency: transitive
description:
@@ -655,10 +663,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
- sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
+ sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
url: "https://pub.dev"
source: hosted
- version: "2.1.3"
+ version: "2.1.7"
pointycastle:
dependency: transitive
description:
@@ -932,10 +940,10 @@ packages:
dependency: transitive
description:
name: win32
- sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
+ sha256: "7dacfda1edcca378031db9905ad7d7bd56b29fd1a90b0908b71a52a12c41e36b"
url: "https://pub.dev"
source: hosted
- version: "3.1.3"
+ version: "5.0.3"
worker_manager:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 26876d35..f1a2a389 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -17,7 +17,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 0.2.7+3
+version: 0.2.8+4
environment:
sdk: ">=2.18.6 <3.0.0"
@@ -73,6 +73,7 @@ dependencies:
introduction_screen: ^3.1.12
app_settings: ^5.1.1
auto_start_flutter: ^0.1.1
+ pick_or_save: ^2.2.4
dev_dependencies:
flutter_test:
diff --git a/test/common/widgets/fields/date_picker_field_test.dart b/test/common/widgets/fields/date_picker_field_test.dart
index cc1a93a7..51406f3b 100644
--- a/test/common/widgets/fields/date_picker_field_test.dart
+++ b/test/common/widgets/fields/date_picker_field_test.dart
@@ -1,4 +1,5 @@
import 'package:clock_app/common/widgets/fields/date_picker_field.dart';
+import 'package:clock_app/theme/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -17,19 +18,19 @@ void main() {
testWidgets('with 1 date', (tester) async {
final value = [DateTime(2021, 1, 1)];
await _renderWidget(tester, value: value);
- final valueFinder = find.byType(DateChip);
+ final valueFinder = find.byKey(const Key("DateChip"));
expect(valueFinder, findsOneWidget);
});
testWidgets('with 2 dates', (tester) async {
final value = [DateTime(2021, 1, 1), DateTime(2021, 1, 2)];
await _renderWidget(tester, value: value);
- final valueFinder = find.byType(DateChip);
+ final valueFinder = find.byKey(const Key("DateChip"));
expect(valueFinder, findsNWidgets(2));
});
testWidgets('with 10 dates', (tester) async {
final value = List.generate(10, (index) => DateTime(2021, 1, 1));
await _renderWidget(tester, value: value);
- final valueFinder = find.byType(DateChip);
+ final valueFinder = find.byKey(const Key("DateChip"));
expect(valueFinder, findsNWidgets(10));
});
});
@@ -75,6 +76,7 @@ Future _renderWidget(WidgetTester tester,
void Function(List)? onChanged}) async {
await tester.pumpWidget(
MaterialApp(
+ theme: defaultTheme,
home: Scaffold(
body: DatePickerField(
value: value,