diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 201ac68..659359e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,17 +43,17 @@ jobs: run: jarsigner --verify --verbose build/app/outputs/flutter-apk/app-release.apk - name: Rename APK file - run: mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/bloomee_tunes_v2.4.3+${{github.run_number}}.apk + run: mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/bloomee_tunes_v2.4.4+${{github.run_number}}.apk - name: Upload Artifacts uses: actions/upload-artifact@v2 with: name: Release path: | - build/app/outputs/flutter-apk/bloomee_tunes_v2.4.3+${{github.run_number}}.apk + build/app/outputs/flutter-apk/bloomee_tunes_v2.4.4+${{github.run_number}}.apk - name: Create Release uses: ncipollo/release-action@v1 with: - artifacts: "build/app/outputs/flutter-apk/bloomee_tunes_v2.4.3+${{github.run_number}}.apk" - tag: v2.4.3+${{github.run_number}} + artifacts: "build/app/outputs/flutter-apk/bloomee_tunes_v2.4.4+${{github.run_number}}.apk" + tag: v2.4.4+${{github.run_number}} token: ${{secrets.SECRET_KEY}} \ No newline at end of file diff --git a/lib/blocs/timer/timer_bloc.dart b/lib/blocs/timer/timer_bloc.dart new file mode 100644 index 0000000..76529ad --- /dev/null +++ b/lib/blocs/timer/timer_bloc.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:Bloomee/blocs/mediaPlayer/bloomee_player_cubit.dart'; +import 'package:Bloomee/main.dart'; +import 'package:Bloomee/utils/ticker.dart'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'timer_event.dart'; +part 'timer_state.dart'; + +class TimerBloc extends Bloc { + final Ticker _ticker; + static const int _duration = 0; + + StreamSubscription? _tickerSubscription; + + TimerBloc({required Ticker ticker, required BloomeePlayerCubit bloomeePlayer}) + : _ticker = ticker, + super(const TimerInitial(_duration)) { + on(_onTimerStarted); + on<_TimerTicked>(_onTimerTicked); + on(onTimerPaused); + on(onTimerResumed); + on(onTimerReset); + on(onTimerStopped); + } + + void _onTimerStarted(TimerStarted event, Emitter emit) { + emit(TimerRunInProgress(event.duration)); + _tickerSubscription?.cancel(); + _tickerSubscription = + _ticker.tick(ticks: event.duration).listen((duration) { + add(_TimerTicked(duration: duration)); + }); + } + + void onTimerPaused(TimerPaused event, Emitter emit) { + if (state is TimerRunInProgress) { + _tickerSubscription?.pause(); + emit(TimerRunPause(state.duration)); + } + } + + void onTimerResumed(TimerResumed event, Emitter emit) { + if (state is TimerRunPause) { + _tickerSubscription?.resume(); + emit(TimerRunInProgress(state.duration)); + } + } + + void onTimerReset(TimerReset event, Emitter emit) { + _tickerSubscription?.cancel(); + emit(const TimerInitial(_duration)); + } + + void onTimerStopped(TimerStopped event, Emitter emit) { + _tickerSubscription?.cancel(); + emit(const TimerRunStopped()); + } + + void _onTimerTicked(_TimerTicked event, Emitter emit) { + // emit(event.duration > 0 + // ? TimerRunInProgress(event.duration) + // : const TimerRunComplete()); + if (event.duration > 0) { + emit(TimerRunInProgress(event.duration)); + } else { + emit(const TimerRunComplete()); + try { + bloomeePlayerCubit.bloomeePlayer.pause(); + } catch (e) { + log(e.toString(), name: "TimerBloc"); + } + } + } + + @override + Future close() { + _tickerSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/blocs/timer/timer_event.dart b/lib/blocs/timer/timer_event.dart new file mode 100644 index 0000000..d44313b --- /dev/null +++ b/lib/blocs/timer/timer_event.dart @@ -0,0 +1,31 @@ +part of 'timer_bloc.dart'; + +sealed class TimerEvent { + const TimerEvent(); +} + +final class TimerStarted extends TimerEvent { + const TimerStarted({required this.duration}); + final int duration; +} + +final class TimerPaused extends TimerEvent { + const TimerPaused(); +} + +final class TimerResumed extends TimerEvent { + const TimerResumed(); +} + +class TimerReset extends TimerEvent { + const TimerReset(); +} + +class TimerStopped extends TimerEvent { + const TimerStopped(); +} + +class _TimerTicked extends TimerEvent { + const _TimerTicked({required this.duration}); + final int duration; +} diff --git a/lib/blocs/timer/timer_state.dart b/lib/blocs/timer/timer_state.dart new file mode 100644 index 0000000..7fe8802 --- /dev/null +++ b/lib/blocs/timer/timer_state.dart @@ -0,0 +1,38 @@ +part of 'timer_bloc.dart'; + +sealed class TimerState extends Equatable { + const TimerState(this.duration); + final int duration; + + @override + List get props => [duration]; +} + +final class TimerInitial extends TimerState { + const TimerInitial(super.duration); + + @override + String toString() => 'TimerInitial { duration: $duration }'; +} + +final class TimerRunPause extends TimerState { + const TimerRunPause(super.duration); + + @override + String toString() => 'TimerRunPause { duration: $duration }'; +} + +final class TimerRunStopped extends TimerState { + const TimerRunStopped() : super(0); +} + +final class TimerRunInProgress extends TimerState { + const TimerRunInProgress(super.duration); + + @override + String toString() => 'TimerRunInProgress { duration: $duration }'; +} + +final class TimerRunComplete extends TimerState { + const TimerRunComplete() : super(0); +} diff --git a/lib/main.dart b/lib/main.dart index 7f6c94d..50f3ebf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,10 +3,12 @@ import 'dart:developer'; import 'dart:io' as io; import 'package:Bloomee/blocs/internet_connectivity/cubit/connectivity_cubit.dart'; import 'package:Bloomee/blocs/settings_cubit/cubit/settings_cubit.dart'; +import 'package:Bloomee/blocs/timer/timer_bloc.dart'; import 'package:Bloomee/screens/widgets/snackbar.dart'; import 'package:Bloomee/theme_data/default.dart'; import 'package:Bloomee/services/file_manager.dart'; import 'package:Bloomee/utils/external_list_importer.dart'; +import 'package:Bloomee/utils/ticker.dart'; import 'package:Bloomee/utils/url_checker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -168,6 +170,9 @@ class _MyAppState extends State { create: (context) => SettingsCubit(), lazy: false, ), + BlocProvider( + create: (context) => TimerBloc( + ticker: const Ticker(), bloomeePlayer: bloomeePlayerCubit)), BlocProvider( create: (context) => ConnectivityCubit(), lazy: false, diff --git a/lib/screens/screen/home_views/timer_view.dart b/lib/screens/screen/home_views/timer_view.dart index 176cb23..14b3680 100644 --- a/lib/screens/screen/home_views/timer_view.dart +++ b/lib/screens/screen/home_views/timer_view.dart @@ -1,19 +1,71 @@ -import 'package:flutter/cupertino.dart'; +import 'dart:async'; +import 'package:Bloomee/blocs/timer/timer_bloc.dart'; +import 'package:Bloomee/screens/widgets/snackbar.dart'; import 'package:flutter/material.dart'; import 'package:Bloomee/theme_data/default.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:numberpicker/numberpicker.dart'; -class TimerView extends StatelessWidget { +class TimerView extends StatefulWidget { const TimerView({super.key}); + @override + State createState() => _TimerViewState(); +} + +class _TimerViewState extends State { + int _currentHour = 0; + int _currentMinute = 0; + int _currentSecond = 0; + StreamSubscription? _timerBlocSubscription; + + void parseDuration(int duration, int Function(int) hourCallback, + int Function(int) minuteCallback, int Function(int) secondCallback) { + final hours = (duration / 3600).floor(); + final minutes = (duration % 3600 / 60).floor(); + final seconds = duration % 60; + + hourCallback(hours); + minuteCallback(minutes); + secondCallback(seconds); + } + + @override + void initState() { + _timerBlocSubscription = context.read().stream.listen((event) { + if (event is TimerRunInProgress) { + setState(() { + parseDuration(event.duration, (p0) => _currentHour = p0, + (p1) => _currentMinute = p1, (p2) => _currentSecond = p2); + }); + } else if (event is TimerInitial || event is TimerRunComplete) { + setState(() { + _currentHour = 0; + _currentMinute = 0; + _currentSecond = 0; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _timerBlocSubscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Default_Theme.themeColor, appBar: AppBar( backgroundColor: Default_Theme.themeColor, + surfaceTintColor: Default_Theme.themeColor, foregroundColor: Default_Theme.primaryColor1, title: Text( - 'Timer', + 'Sleep Timer', style: const TextStyle( color: Default_Theme.primaryColor1, fontSize: 25, @@ -21,8 +73,341 @@ class TimerView extends StatelessWidget { .merge(Default_Theme.secondoryTextStyle), ), ), - body: const Center( - child: Text('Bloomee Timer View'), + body: Center( + child: BlocBuilder( + builder: (context, state) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: switch (state) { + TimerInitial() => timerInitial(), + TimerRunInProgress() => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 60, left: 15, right: 15), + child: Text( + "Preparing for a peaceful interlude in…", + maxLines: 2, + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 25, + fontWeight: FontWeight.bold) + .merge(Default_Theme.secondoryTextStyle), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + timerLabel(label: "Hours", time: _currentHour), + timerLabel(label: "Minutes", time: _currentMinute), + timerLabel(label: "Seconds", time: _currentSecond), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: ElevatedButton( + onPressed: () { + context + .read() + .add(const TimerStopped()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Default_Theme.accentColor2, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(right: 10), + child: Icon( + MingCute.stop_circle_fill, + color: Default_Theme.primaryColor2, + size: 40, + ), + ), + Text( + "Stop Timer", + style: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 20, + fontWeight: FontWeight.bold) + .merge(Default_Theme.secondoryTextStyle), + ), + ], + ), + ), + ), + ], + ), + ), + TimerRunPause() => Container(), + TimerRunStopped() => timerInitial(), + TimerRunComplete() => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: + EdgeInsets.only(bottom: 60, left: 10, right: 10), + child: Text( + "The tunes have rested. Sweet Dreams 🥰.", + textAlign: TextAlign.center, + style: TextStyle( + color: Default_Theme.accentColor2, + fontSize: 40, + fontFamily: "Unageo", + fontWeight: FontWeight.bold), + ), + ), + ElevatedButton( + onPressed: () { + context.read().add(const TimerReset()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Default_Theme.accentColor2, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Text( + "Got it!", + style: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 15, + fontWeight: FontWeight.bold) + .merge(Default_Theme.secondoryTextStyle), + ), + ), + ], + )), + _ => const Center(child: CircularProgressIndicator()) + }, + ); + }, + ), + ), + ); + } + + Widget timerInitial() { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 270, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Hours", + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, fontSize: 25) + .merge(Default_Theme.secondoryTextStyleMedium)), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: NumberPicker( + minValue: 0, + maxValue: 23, + itemHeight: 80, + zeroPad: true, + infiniteLoop: true, + value: _currentHour, + textStyle: TextStyle( + color: Default_Theme.primaryColor2 + .withOpacity(0.7), + fontSize: 20) + .merge(Default_Theme.secondoryTextStyle), + selectedTextStyle: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 40) + .merge(Default_Theme.secondoryTextStyleMedium), + // zeroPad: true, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Default_Theme.primaryColor2.withOpacity(0.07), + borderRadius: BorderRadius.circular(16), + // border: Border.all(color: Default_Theme.primaryColor2), + ), + onChanged: (int value) { + setState(() => _currentHour = value); + }, + ), + ), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Minutes", + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, fontSize: 25) + .merge(Default_Theme.secondoryTextStyleMedium)), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: NumberPicker( + minValue: 0, + maxValue: 59, + itemHeight: 80, + zeroPad: true, + infiniteLoop: true, + value: _currentMinute, + textStyle: TextStyle( + color: Default_Theme.primaryColor2 + .withOpacity(0.7), + fontSize: 20) + .merge(Default_Theme.secondoryTextStyle), + selectedTextStyle: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 40) + .merge(Default_Theme.secondoryTextStyleMedium), + // zeroPad: true, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Default_Theme.primaryColor2.withOpacity(0.07), + borderRadius: BorderRadius.circular(16), + // border: Border.all(color: Default_Theme.primaryColor2), + ), + onChanged: (int value) { + setState(() => _currentMinute = value); + }, + ), + ), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Seconds", + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, fontSize: 25) + .merge(Default_Theme.secondoryTextStyleMedium)), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: NumberPicker( + minValue: 0, + maxValue: 59, + itemHeight: 80, + zeroPad: true, + infiniteLoop: true, + value: _currentSecond, + textStyle: TextStyle( + color: Default_Theme.primaryColor2 + .withOpacity(0.7), + fontSize: 20) + .merge(Default_Theme.secondoryTextStyle), + selectedTextStyle: const TextStyle( + color: Default_Theme.primaryColor2, + fontSize: 40) + .merge(Default_Theme.secondoryTextStyleMedium), + // zeroPad: true, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Default_Theme.primaryColor2.withOpacity(0.07), + borderRadius: BorderRadius.circular(16), + // border: Border.all(color: Default_Theme.primaryColor2), + ), + onChanged: (int value) { + setState(() => _currentSecond = value); + }, + ), + ), + ), + ], + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Default_Theme.accentColor2, + padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + onPressed: () { + if (_currentHour != 0 || + _currentMinute != 0 || + _currentSecond != 0) { + context.read().add(TimerStarted( + duration: (_currentHour * 3600) + + (_currentMinute * 60) + + _currentSecond)); + } else { + SnackbarService.showMessage("Please set a time", + duration: const Duration(seconds: 1)); + } + }, + child: Text( + "Start Timer", + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold) + .merge(Default_Theme.secondoryTextStyle), + ), + ), + ), + ], + ); + } + + Widget timerLabel({required String label, required int time}) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, fontSize: 25) + .merge(Default_Theme.secondoryTextStyleMedium)), + Container( + width: 90, + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Default_Theme.primaryColor2.withOpacity(0.07), + borderRadius: BorderRadius.circular(16), + // border: Border.all(color: Default_Theme.primaryColor2), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text(time.toString().padLeft(2, '0'), + textAlign: TextAlign.center, + style: const TextStyle( + color: Default_Theme.primaryColor2, fontSize: 35) + .merge(Default_Theme.secondoryTextStyleMedium)), + ), + ), + ], ), ); } diff --git a/lib/theme_data/default.dart b/lib/theme_data/default.dart index cadd098..d1c82ca 100644 --- a/lib/theme_data/default.dart +++ b/lib/theme_data/default.dart @@ -15,7 +15,7 @@ class Default_Theme { //Colors static const themeColor = Color(0xFF0A040C); static const primaryColor1 = Color(0xFFDAEAF7); - static const primaryColor2 = Color(0xFFDDCAD9); + static const primaryColor2 = Color.fromARGB(255, 242, 231, 240); static const accentColor1 = Color(0xFF0EA5E0); static const accentColor1light = Color(0xFF18C9ED); static const accentColor2 = Color(0xFFFE385E); diff --git a/lib/utils/ticker.dart b/lib/utils/ticker.dart new file mode 100644 index 0000000..6f8f357 --- /dev/null +++ b/lib/utils/ticker.dart @@ -0,0 +1,16 @@ +class Ticker { + const Ticker(); + Stream tick({required int ticks}) { + return Stream.periodic(const Duration(seconds: 1), (x) => ticks - x - 1) + .take(ticks); + } + + Stream tickHMS( + {required int hours, + required int minutes, + required int seconds, + required Function(int) onTick}) { + final totalSeconds = (hours * 3600) + (minutes * 60) + seconds; + return tick(ticks: totalSeconds); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a783c23..188015a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,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: 2.4.3+0 +version: 2.4.4+0 environment: sdk: '>=3.0.6 <4.0.0' @@ -65,6 +65,7 @@ dependencies: receive_sharing_intent: ^1.6.7 file_picker: ^8.0.0+1 fuzzywuzzy: ^1.1.6 + numberpicker: ^2.1.2