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,