diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart index ffb055b5..66423d62 100644 --- a/lib/alarm/data/alarm_settings_schema.dart +++ b/lib/alarm/data/alarm_settings_schema.dart @@ -10,6 +10,7 @@ import 'package:clock_app/alarm/types/schedules/weekly_alarm_schedule.dart'; import 'package:clock_app/alarm/widgets/alarm_task_card.dart'; import 'package:clock_app/alarm/widgets/try_alarm_task_button.dart'; import 'package:clock_app/audio/audio_channels.dart'; +import 'package:clock_app/audio/screens/record_ringtone_screen.dart'; import 'package:clock_app/audio/screens/ringtones_screen.dart'; import 'package:clock_app/audio/types/ringtone_player.dart'; import 'package:clock_app/common/data/weekdays.dart'; @@ -167,6 +168,16 @@ SettingGroup alarmSettingsSchema = SettingGroup( RingtonePlayer.stop(); }, actions: [ + MenuAction( + "Record", + (context) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const RecordRingtoneScreen()), + ); + }, + Icons.mic, + ), MenuAction( "Add", (context) async { diff --git a/lib/audio/screens/record_ringtone_screen.dart b/lib/audio/screens/record_ringtone_screen.dart new file mode 100644 index 00000000..036ffee9 --- /dev/null +++ b/lib/audio/screens/record_ringtone_screen.dart @@ -0,0 +1,138 @@ +import 'dart:io'; +import 'dart:math'; +import 'package:clock_app/audio/types/ringtone_player.dart'; +import 'package:clock_app/common/types/file_item.dart'; +import 'package:clock_app/common/utils/list_storage.dart'; +import 'package:clock_app/common/utils/snackbar.dart'; +import 'package:clock_app/navigation/widgets/app_top_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; +import 'package:record/record.dart'; + +class RecordRingtoneScreen extends StatefulWidget { + const RecordRingtoneScreen({ + super.key, + }); + + @override + State createState() => _RecordRingtoneScreenState(); +} + +class _RecordRingtoneScreenState extends State { + bool recording = false; + DateTime? recordingStart; + late Record recorder; + + @override + void initState() { + recorder = Record(); + super.initState(); + } + + @override + void dispose() { + RingtonePlayer.stop(); + recorder.dispose(); + super.dispose(); + } + + void _toggleRecord(BuildContext context) { + if (recording) { + _stopRecord(context); + } else { + _beginRecord(context); + } + } + + void _stopRecord(BuildContext context) async { + if (!recording || recordingStart == null) return; + + try { + final filename = (await recorder.stop())!; + final file = File(filename); + final bytes = await file.readAsBytes(); + + final ringtoneList = await loadList("ringtones"); + final uri = await saveRingtone(path.basename(filename), bytes); + ringtoneList.add( + FileItem("Recording from ${recordingStart.toString()}", uri, + FileItemType.audio), + ); + await saveList("ringtones", ringtoneList); + if(context.mounted) { + showSnackBar(context, AppLocalizations.of(context)!.melodyRecorderOnSaved); + } + } catch (ex) { + //this version of `record` is the latest that works + //with sdk level 21, but it has an issue where spurious + //errors can be thrown when stopping the record. + if(context.mounted) { + showSnackBar(context, AppLocalizations.of(context)!.melodyRecorderOnError, error: true); + } + } finally { + setState(() { + recording = false; + }); + } + } + + void _beginRecord(BuildContext context) async { + if (recording) return; + + if (await recorder.hasPermission()) { + setState(() { + recording = true; + recordingStart = DateTime.now(); + }); + + final folderPath = await getTemporaryDirectory(); + final time = recordingStart! + .toLocal() + .toIso8601String() + .replaceAll(RegExp(r'[^0-9]'), "-"); + final rand = Random().nextInt(255).toRadixString(16); + + final filename = path.join(folderPath.path, "Recording-$time-$rand.m4a"); + + await recorder.start(path: filename); + if(context.mounted) { + showSnackBar(context, AppLocalizations.of(context)!.melodyRecorderOnStart); + } + } + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + TextTheme textTheme = theme.textTheme; + + return Scaffold( + appBar: AppTopBar( + title: AppLocalizations.of(context)!.melodiesSetting, + ), + body: Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Material( + color: Colors.redAccent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + highlightColor: Colors.red, + splashColor: Colors.red, + onTap: () => _toggleRecord(context), + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Icon(recording ? Icons.stop : Icons.mic, + size: 100)))), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/lib/common/widgets/color_box.dart b/lib/common/widgets/color_box.dart index 6eae2f7a..82fe0c04 100644 --- a/lib/common/widgets/color_box.dart +++ b/lib/common/widgets/color_box.dart @@ -7,7 +7,7 @@ class ColorBox extends StatelessWidget { @override Widget build(BuildContext context) { - CardTheme cardTheme = Theme.of(context).cardTheme; + CardThemeData cardTheme = Theme.of(context).cardTheme; return Container( width: 36.0, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4fd37495..96d06532 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -778,5 +778,8 @@ "backgroundServiceIntervalSettingDescription": "Lower interval will help keep the app alive, at the cost of some battery life", "custom": "Custom", "app": "App", - "materialYou": "Material You" + "materialYou": "Material You", + "melodyRecorderOnStart": "Started recording", + "melodyRecorderOnSaved": "Saved recording to melodies", + "melodyRecorderOnError": "Error saving recording. Please retry" } diff --git a/lib/settings/screens/list_filter_settings_screen.dart b/lib/settings/screens/list_filter_settings_screen.dart deleted file mode 100644 index 2d8cfb58..00000000 --- a/lib/settings/screens/list_filter_settings_screen.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:clock_app/common/types/tag.dart'; -import 'package:clock_app/common/widgets/fab.dart'; -import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; -import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; -import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/widgets/tag_card.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ListFilterSettingsScreen extends StatefulWidget { - const ListFilterSettingsScreen({ - super.key, - }); - - @override - State createState() => - _ListFilterSettingsScreenState(); -} - -class _ListFilterSettingsScreenState extends State { - final _listController = PersistentListController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), - body: Stack( - children: [ - Column( - children: [ - Expanded( - child: PersistentListView( - saveTag: 'tags', - listController: _listController, - itemBuilder: (tag) => TagCard( - key: ValueKey(tag), - tag: tag, - onPressDelete: () => _listController.deleteItem(tag), - onPressDuplicate: () => _listController.duplicateItem(tag), - ), - onTapItem: (tag, index) async { - Tag? newTag = await showTagEditor(tag); - if (newTag == null) return; - tag.copyFrom(newTag); - _listController.changeItems((tags) {}); - }, - // onDeleteItem: _handleDeleteTimer, - placeholderText: "No tags created", - reloadOnPop: true, - isSelectable: true, - ), - ), - ], - ), - FAB( - bottomPadding: 8, - onPressed: () async { - Tag? tag = await showTagEditor(); - if (tag == null) return; - _listController.addItem(tag); - }, - ) - ], - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index a3c9c2ad..f3c5c9bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -474,10 +474,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" flutter_system_ringtones: dependency: "direct main" description: @@ -621,18 +621,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -693,18 +693,18 @@ packages: dependency: "direct main" description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: "direct main" description: @@ -953,6 +953,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.5" + record: + dependency: "direct main" + description: + name: record + sha256: "7d29f4d8a27caafa6d5c0740aeefb49a73f995b7c385a469d9ef7b3e3764035c" + url: "https://pub.dev" + source: hosted + version: "4.3.3" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "4e1e22666956c360a3c4b0c32651610fd9591d339fc5771153c549756dfc775d" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + record_macos: + dependency: transitive + description: + name: record_macos + sha256: "1b107e05c04ee93846b30b7778f6f0b89a33b994a336eac4f489540226202dc6" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "6dcae5252ff7157c5f5b3117014cc4c4e267736042b13b5bf736518cddf401a6" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: "2954f67af2c2aef960cbd8c8fbefaeee0244697d9a7d6b15d887e54d9301372b" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: e0b78b6f336880852257d1fe8d06441d1bdb24524c6a9c3aee562e4a7232152b + url: "https://pub.dev" + source: hosted + version: "0.6.1" rxdart: dependency: transitive description: @@ -1029,7 +1077,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -1058,10 +1106,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1074,10 +1122,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: @@ -1106,10 +1154,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" timer_builder: dependency: "direct main" description: @@ -1226,10 +1274,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" watcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d9a8519e..5442ddcb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: path_provider: ^2.0.11 path: ^1.8.2 sqflite: ^2.2.2 - flutter_slidable: ^3.1.0 + flutter_slidable: ^3.1.2 flutter_system_ringtones: ^0.0.6 # android_alarm_manager_plus: ^4.0.4 android_alarm_manager_plus: @@ -63,7 +63,7 @@ dependencies: receive_intent: ^0.2.5 watcher: ^1.1.0 dynamic_color: ^1.7.0 - material_color_utilities: ^0.8.0 + material_color_utilities: ^0.11.1 flutter_oss_licenses: ^3.0.2 locale_names: ^1.1.1 # home_widget: ^0.5.0 @@ -88,6 +88,8 @@ dependencies: mime: ^1.0.6 analog_clock: ^0.1.1 animated_analog_clock: ^0.1.0 + #Latest version that supports current Android SDK version + record: 4.3.3 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list"