From fe38637e22c48a9a6bb17e164073d09b8e924929 Mon Sep 17 00:00:00 2001 From: Christopher Hartono <102641262+bootloopmaster636@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:17:51 +0700 Subject: [PATCH 1/2] Dev chto (#27) --- lib/displayLayers/OverlayLayer.dart | 39 ++ lib/displayLayers/TopLayer.dart | 471 ++++++++++++++++--------- lib/logic/managers/DisplayManager.dart | 17 + lib/logic/states/TimerState.dart | 47 ++- 4 files changed, 401 insertions(+), 173 deletions(-) diff --git a/lib/displayLayers/OverlayLayer.dart b/lib/displayLayers/OverlayLayer.dart index 95102f9..b191a74 100644 --- a/lib/displayLayers/OverlayLayer.dart +++ b/lib/displayLayers/OverlayLayer.dart @@ -94,6 +94,7 @@ class SettingsPanelInside extends ConsumerWidget { AssistTimerSection(), BonusTimerSection(), CutOffTimerSection(), + StartAtSection(), SectionTitle(title: "Display"), ThemeModeSection(), DisplayScaleFactorSection(), @@ -221,6 +222,44 @@ class TitleSection extends ConsumerWidget { } } +class StartAtSection extends ConsumerWidget { + const StartAtSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final timerWatcher = ref.watch(timerProvider); + final timerManager = ref.watch(timerProvider).timerManager; + + return Card( + child: Column( + children: [ + ListTile( + title: const Text("Start timer at"), + subtitle: const Text("Will start timer automatically at this time"), + trailing: Switch( + value: ref.watch(timerProvider).isAutoStartEnabled, + onChanged: (value) { + ref.read(timerProvider).toggleAutoStart(); + }, + ), + ), + ListTile( + enabled: ref.watch(timerProvider).isAutoStartEnabled, + title: const Text("Set time"), + trailing: Text( + "${ref.watch(timerProvider).autoStartTime.hour.toString().padLeft(2, '0')} : ${ref.watch(timerProvider).autoStartTime.minute.toString().padLeft(2, '0')}", + style: const TextStyle(fontSize: 20), + ), + onTap: () async { + ref.read(timerProvider).setStartAt(await showTimePickerDialog(context)); + }, + ), + ], + ), + ); + } +} + class MainTimerSection extends ConsumerWidget { const MainTimerSection({super.key}); diff --git a/lib/displayLayers/TopLayer.dart b/lib/displayLayers/TopLayer.dart index 0ffee53..5c7a276 100644 --- a/lib/displayLayers/TopLayer.dart +++ b/lib/displayLayers/TopLayer.dart @@ -14,6 +14,7 @@ class TopLayer extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final scaleFactor = ref.watch(displayStateProvider).displayFontScale; final displayStateWatcher = ref.watch(displayStateProvider); + final isNoteVisible = ref.watch(timerProvider).displayManager.isNoteVisible; return Animate( effects: const [ @@ -21,31 +22,152 @@ class TopLayer extends ConsumerWidget { duration: Duration(milliseconds: 450), curve: Curves.easeInOutCubic, begin: Offset(0.0, 0.0), - end: Offset(0.04, 0.0) - ), + end: Offset(0.04, 0.0)), FadeEffect( duration: Duration(milliseconds: 400), curve: Curves.ease, begin: 1.0, - end: 0.4 - ), + end: 0.6), ], target: (displayStateWatcher.settingsExpanded == true) ? 1 : 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + child: Row( children: [ - const TopBar(), - SizedBox( - height: MediaQuery.of(context).size.height - 60 * scaleFactor, - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, + AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutQuad, + width: isNoteVisible + ? MediaQuery.of(context).size.width * 0.6 + : MediaQuery.of(context).size.width, + child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - TimerCard(), - InfoCard(), + const TopBar(), + SizedBox( + height: MediaQuery.of(context).size.height - 60 * scaleFactor, + child: const Stack(children: [ + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TimerCard(), + InfoCard(), + ], + ), + ), + FullscreenFAB(), + ]), + ), ], ), ), + const NotePanel() + ], + ), + ); + } +} + +class FullscreenFAB extends ConsumerWidget { + const FullscreenFAB({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isFullScreenNotifier = ref.watch(fullscreenProvider); + void showToastLocal(String msg) { + showToast(msg, context: context); + } + + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(24.0), + child: FloatingActionButton.small( + backgroundColor: Theme.of(context).colorScheme.surface, + onPressed: () async { + bool currentFullScreen = + await WindowManager.instance.isFullScreen(); + + WindowManager.instance.setFullScreen(!currentFullScreen); + showToastLocal(currentFullScreen + ? "Window has been restored" + : "Window has entered fullscreen mode"); + + isFullScreenNotifier.value = !currentFullScreen; + }, + child: Icon( + isFullScreenNotifier.value + ? Icons.fullscreen_exit + : Icons.fullscreen, + ), + ), + ), + ); + } +} + +class NotePanel extends ConsumerStatefulWidget { + const NotePanel({super.key}); + + @override + NotePanelState createState() => NotePanelState(); +} + +class NotePanelState extends ConsumerState { + final TextEditingController _noteController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final scaleFactor = MediaQuery.of(context).textScaleFactor; + + return AnimatedContainer( + width: ref.watch(timerProvider).displayManager.isNoteVisible ? MediaQuery.of(context).size.width * 0.4 : 0, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + ), + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutQuad, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 60 * scaleFactor, + width: MediaQuery.of(context).size.width * 0.4, + color: Theme.of(context) + .colorScheme + .tertiaryContainer + .withOpacity(0.6), + child: const Center( + child: Text( + "Note", + style: TextStyle(fontSize: 24), + )), + ), + Expanded( + child: SizedBox( + height: MediaQuery.of(context).size.height - 60 * scaleFactor, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _noteController, + maxLines: null, + expands: true, + decoration: const InputDecoration.collapsed( + hintText: "Enter your note", + ), + style: const TextStyle( + fontSize: 20, + ), + onChanged: (note) { + ref.read(timerProvider).setNote(note); + }, + ), + ), + ), + ) ], ), ); @@ -61,11 +183,6 @@ class TopBar extends ConsumerWidget { final timerWatcher = ref.watch(timerProvider); final timerManager = ref.watch(timerProvider).timerManager; final displayStateWatcher = ref.watch(displayStateProvider); - final isFullScreenNotifier = ref.watch(fullscreenProvider); - - void showToastLocal(String msg) { - showToast(msg, context: context); - } // not defining ref.read(...) into a variable because documentation said it's bad practice, and causing bugs @@ -95,25 +212,15 @@ class TopBar extends ConsumerWidget { ), InkWell( splashFactory: InkRipple.splashFactory, - onTap: () async { - bool currentFullScreen = - await WindowManager.instance.isFullScreen(); - - WindowManager.instance.setFullScreen(!currentFullScreen); - showToastLocal(currentFullScreen - ? "Window has been restored" - : "Window has been maximized"); - - isFullScreenNotifier.value = !currentFullScreen; + onTap: () { + ref.read(timerProvider).toggleNoteVisibility(); }, child: Container( width: 60 * scaleFactor, height: 60 * scaleFactor, color: Theme.of(context).colorScheme.secondary, child: Icon( - isFullScreenNotifier.value - ? Icons.fullscreen_exit - : Icons.fullscreen, + Icons.event_note_outlined, size: 22 * scaleFactor, color: Theme.of(context).colorScheme.onSecondary, ), @@ -177,7 +284,7 @@ class TopBar extends ConsumerWidget { child: Container( width: 60 * scaleFactor, height: 60 * scaleFactor, - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.tertiary, child: Icon( Icons.replay, size: 22 * scaleFactor, @@ -200,36 +307,43 @@ class TimerCard extends ConsumerWidget { final timerWatcher = ref.watch(timerProvider); final timerManager = ref.watch(timerProvider).timerManager; - return Container( - margin: EdgeInsets.only(top: 20 * scaleFactor), - width: 800 * scaleFactor, - height: 220 * scaleFactor, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background.withOpacity(0.9), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.primary.withOpacity(0.4), - blurRadius: 8, - spreadRadius: 2, - offset: const Offset(0, 1), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + margin: EdgeInsets.only(top: 20 * scaleFactor), + width: 680 * scaleFactor, + height: 220 * scaleFactor, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background.withOpacity(0.9), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + blurRadius: 8, + spreadRadius: 2, + offset: const Offset(0, 1), + ), + ], ), - ], - ), - child: Center( - child: Text( - "${timerManager.getTimer(TimerType.main).inHours.toString().padLeft(2, '0')}:" - "${timerManager.getTimer(TimerType.main).inMinutes.remainder(60).toString().padLeft(2, '0')}:" - "${timerManager.getTimer(TimerType.main).inSeconds.remainder(60).toString().padLeft(2, '0')}", - style: TextStyle( - fontSize: 116, - fontWeight: FontWeight.w600, - color: (timerWatcher.isCutOffRunning) && - (timerManager.getTimer(TimerType.main).inSeconds % 2 == 0) - ? Colors.red - : null, + child: Center( + child: Text( + "${timerManager.getTimer(TimerType.main).inHours.toString().padLeft(2, '0')}:" + "${timerManager.getTimer(TimerType.main).inMinutes.remainder(60).toString().padLeft(2, '0')}:" + "${timerManager.getTimer(TimerType.main).inSeconds.remainder(60).toString().padLeft(2, '0')}", + style: TextStyle( + fontSize: 116, + fontWeight: FontWeight.w600, + color: (timerWatcher.isCutOffRunning) && + (timerManager.getTimer(TimerType.main).inSeconds % 2 == + 0) + ? Colors.red + : null, + ), + overflow: TextOverflow.ellipsis, + ), ), - overflow: TextOverflow.ellipsis, ), ), ); @@ -245,126 +359,139 @@ class InfoCard extends ConsumerWidget { final timerWatcher = ref.watch(timerProvider); final timerManager = ref.watch(timerProvider).timerManager; - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - Text( - "${timerWatcher.isCutOffRunning ? "Cut Off" : "Pengumpulan"} pada pukul", - style: TextStyle( - fontSize: - timerWatcher.isCutOffRunning ? 36 : 28 * scaleFactor, - fontWeight: FontWeight.w600, - ), - ), - Text( - "${timerManager.endAt.hour.toString().padLeft(2, '0')}:${timerManager.endAt.minute.toString().padLeft(2, '0')}", - style: TextStyle( - fontSize: 48 * scaleFactor, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - ), - Text( - timerManager.isTimerSet(TimerType.cutoff) - ? "Cutoff di-set selama ${timerManager.getTimer(TimerType.cutoff).inMinutes} menit" - : "", - style: TextStyle( - fontSize: 20 * scaleFactor, - color: Theme.of(context).colorScheme.onTertiaryContainer, - ), - ) - ], - ), - ], - ), - SizedBox( - height: 24 * scaleFactor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - FontAwesomeIcons.personCircleQuestion, - size: 64 * scaleFactor, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - SizedBox( - width: 24 * scaleFactor, - ), - RichText( - text: TextSpan( - text: "Dapat bertanya asisten setelah\n", - style: TextStyle( - fontSize: 24 * scaleFactor, - fontFamily: - Theme.of(context).textTheme.displayMedium!.fontFamily, - color: Theme.of(context).colorScheme.onTertiaryContainer), + return FittedBox( + fit: BoxFit.contain, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 64), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + constraints: BoxConstraints(maxWidth: 400 * scaleFactor), + child: FittedBox( + fit: BoxFit.fitWidth, + child: Column( children: [ - TextSpan( - text: - "${timerManager.getTimer(TimerType.assist).inMinutes.toString().padLeft(2, '0')} menit " - "${timerManager.getTimer(TimerType.assist).inSeconds.remainder(60).toString().padLeft(2, '0')} detik", + Text( + "${timerWatcher.isCutOffRunning ? "Cut Off" : "Pengumpulan"} pada pukul", style: TextStyle( - fontSize: 32 * scaleFactor, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer), + fontSize: (timerWatcher.isCutOffRunning ? 36 : 28) * + scaleFactor, + fontWeight: FontWeight.w600, + ), ), + Text( + "${timerManager.endAt.hour.toString().padLeft(2, '0')}:${timerManager.endAt.minute.toString().padLeft(2, '0')}", + style: TextStyle( + fontSize: 48 * scaleFactor, + fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + (timerManager.isTimerSet(TimerType.cutoff)) + ? Text( + "Cutoff di-set selama ${timerManager.getTimer(TimerType.cutoff).inMinutes} menit", + style: TextStyle( + fontSize: 20 * scaleFactor, + color: Theme.of(context) + .colorScheme + .onTertiaryContainer, + ), + ) + : Container(), ], ), ), - Container( - padding: EdgeInsets.symmetric(horizontal: 40 * scaleFactor), - height: 72 * scaleFactor, - child: VerticalDivider( - color: Theme.of(context).colorScheme.onBackground, - thickness: 6, + ), + SizedBox( + height: 32 * scaleFactor, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + FontAwesomeIcons.personCircleQuestion, + size: 64 * scaleFactor, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), - ), - Icon( - FontAwesomeIcons.anglesUp, - size: 64 * scaleFactor, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - SizedBox( - width: 12 * scaleFactor, - ), - RichText( - text: TextSpan( - text: "Sisa waktu bonus\n", - style: TextStyle( - fontFamily: - Theme.of(context).textTheme.displayMedium!.fontFamily, - fontSize: 24 * scaleFactor, - color: Theme.of(context).colorScheme.onTertiaryContainer), - children: [ - TextSpan( - text: - "${timerManager.getTimer(TimerType.bonus).inMinutes.toString().padLeft(2, '0')} menit " - "${timerManager.getTimer(TimerType.bonus).inSeconds.remainder(60).toString().padLeft(2, '0')} detik", - style: TextStyle( - fontSize: 32 * scaleFactor, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer), - ), - ], + SizedBox( + width: 24 * scaleFactor, ), - ) - ], - ), - ], + RichText( + text: TextSpan( + text: "Dapat bertanya asisten setelah\n", + style: TextStyle( + fontSize: 22 * scaleFactor, + fontFamily: Theme.of(context) + .textTheme + .displayMedium! + .fontFamily, + color: + Theme.of(context).colorScheme.onTertiaryContainer), + children: [ + TextSpan( + text: + "${timerManager.getTimer(TimerType.assist).inMinutes.toString().padLeft(2, '0')} menit " + "${timerManager.getTimer(TimerType.assist).inSeconds.remainder(60).toString().padLeft(2, '0')} detik", + style: TextStyle( + fontSize: 28 * scaleFactor, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40 * scaleFactor), + height: 72 * scaleFactor, + child: VerticalDivider( + color: Theme.of(context).colorScheme.onBackground, + thickness: 6, + ), + ), + Icon( + FontAwesomeIcons.anglesUp, + size: 64 * scaleFactor, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + SizedBox( + width: 12 * scaleFactor, + ), + RichText( + text: TextSpan( + text: "Sisa waktu bonus\n", + style: TextStyle( + fontFamily: Theme.of(context) + .textTheme + .displayMedium! + .fontFamily, + fontSize: 22 * scaleFactor, + color: + Theme.of(context).colorScheme.onTertiaryContainer), + children: [ + TextSpan( + text: + "${timerManager.getTimer(TimerType.bonus).inMinutes.toString().padLeft(2, '0')} menit " + "${timerManager.getTimer(TimerType.bonus).inSeconds.remainder(60).toString().padLeft(2, '0')} detik", + style: TextStyle( + fontSize: 28 * scaleFactor, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), + ), + ], + ), + ) + ], + ), + ], + ), ), ); } diff --git a/lib/logic/managers/DisplayManager.dart b/lib/logic/managers/DisplayManager.dart index ed979f9..fa2304f 100644 --- a/lib/logic/managers/DisplayManager.dart +++ b/lib/logic/managers/DisplayManager.dart @@ -4,17 +4,34 @@ class DisplayManager { DisplayManager(this._title, this._currentAccent, this._currentThemeMode); String _title = ""; + String _note = ""; + bool _isNoteVisible = false; Color _currentAccent = Colors.lightBlue; ThemeMode _currentThemeMode = ThemeMode.system; +// ============== Getters ============= String get title => _title; + + String get note => _note; + + bool get isNoteVisible => _isNoteVisible; + Color get currentAccent => _currentAccent; + ThemeMode get currentThemeMode => _currentThemeMode; void setTitle(String title) { _title = title; } + void setNote(String note) { + _note = note; + } + + void toggleNoteVisibility() { + _isNoteVisible = !_isNoteVisible; + } + void accentCutOff() { _currentAccent = Colors.amber; } diff --git a/lib/logic/states/TimerState.dart b/lib/logic/states/TimerState.dart index c5b6853..62761cc 100644 --- a/lib/logic/states/TimerState.dart +++ b/lib/logic/states/TimerState.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:ugd_timer/logic/managers/DisplayManager.dart'; import 'package:pausable_timer/pausable_timer.dart'; @@ -16,6 +18,9 @@ class TimerState extends ChangeNotifier { late PausableTimer timer = PausableTimer(const Duration(seconds: 1), () {}); // ============= Variables ============= + bool _isAutoStartEnabled = false; + TimeOfDay _autoStartTime = const TimeOfDay(hour: 0, minute: 0); + bool _isRunning = false; bool _isSet = false; bool _isCutOff = false; @@ -24,20 +29,33 @@ class TimerState extends ChangeNotifier { bool _isAllTimerFinishedSoundPlayed = false; // ============== Getters ============= + bool get isAutoStartEnabled => _isAutoStartEnabled; + bool get isRunning => _isRunning; + bool get isCutOffRunning => _isCutOffRunning; + bool get isSet => _isSet; NotificationManager get notificationManager => _notificationManager; + TimerManager get timerManager => _timerManager; + DisplayManager get displayManager => _displayManager; + TimeOfDay get autoStartTime => _autoStartTime; + // ============= Timer Manager Wrapper ============= void setTimer(TimerType timerType, TimeOfDay? timeFromPicker) { _timerManager.setTimerFromPicker(timerType, timeFromPicker); notifyListeners(); } + void setStartAt(TimeOfDay? timeFromPicker) { + _autoStartTime = timeFromPicker!; + notifyListeners(); + } + // ========== Timer Control =========== void startTimer() async { if (!_timerManager.isTimerSet(TimerType.main)) { @@ -59,7 +77,7 @@ class TimerState extends ChangeNotifier { _isCutOff = true; } - startCountdown(); + checkAutoStart(); } } @@ -78,6 +96,11 @@ class TimerState extends ChangeNotifier { notifyListeners(); } + void toggleAutoStart() async { + _isAutoStartEnabled = !_isAutoStartEnabled; + notifyListeners(); + } + void stopAndResetTimer({bool isPressed = false}) { timer.cancel(); @@ -103,6 +126,18 @@ class TimerState extends ChangeNotifier { notifyListeners(); } + void checkAutoStart() async { + while (_isAutoStartEnabled) { + if (_autoStartTime != TimeOfDay.now()) { + print("Waiting until $_autoStartTime"); + await Future.delayed(const Duration(seconds: 1)); + } else { + _isAutoStartEnabled = false; + startCountdown(); + } + } + } + void startCountdown() async { _timerManager.makeEndAt(); @@ -161,6 +196,16 @@ class TimerState extends ChangeNotifier { } // ============= App config interface ============= + void toggleNoteVisibility() { + _displayManager.toggleNoteVisibility(); + notifyListeners(); + } + + void setNote(String note) { + _displayManager.setNote(note); + notifyListeners(); + } + void setTitle(String s) { _displayManager.setTitle(s); notifyListeners(); From dce7cd39a5c9ffa0b8259b8c49ccae837f63f957 Mon Sep 17 00:00:00 2001 From: Christopher Hartono <102641262+bootloopmaster636@users.noreply.github.com> Date: Mon, 16 Oct 2023 07:06:02 +0700 Subject: [PATCH 2/2] bump ver (#28) --- lib/displayLayers/OverlayLayer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/displayLayers/OverlayLayer.dart b/lib/displayLayers/OverlayLayer.dart index b191a74..3510e48 100644 --- a/lib/displayLayers/OverlayLayer.dart +++ b/lib/displayLayers/OverlayLayer.dart @@ -690,7 +690,7 @@ class AboutUs extends StatelessWidget { height: 24, ), const Text( - "Version 0.8.0", + "Version 0.9.1", textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ),