diff --git a/lib/presentation/home/bloc/weekly_schedules_bloc.dart b/lib/presentation/home/bloc/weekly_schedules_bloc.dart index 67fd2e39..1519accf 100644 --- a/lib/presentation/home/bloc/weekly_schedules_bloc.dart +++ b/lib/presentation/home/bloc/weekly_schedules_bloc.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; diff --git a/lib/presentation/home/bloc/weekly_schedules_state.dart b/lib/presentation/home/bloc/weekly_schedules_state.dart index 5098d3b3..7a9d5cdf 100644 --- a/lib/presentation/home/bloc/weekly_schedules_state.dart +++ b/lib/presentation/home/bloc/weekly_schedules_state.dart @@ -11,6 +11,15 @@ final class WeeklySchedulesState extends Equatable { List get dates => schedules.map((schedule) => schedule.scheduleTime).toList(); + ScheduleEntity? get todaySchedule => schedules + .where((schedule) { + final now = DateTime.now(); + return schedule.scheduleTime.year == now.year && + schedule.scheduleTime.month == now.month && + schedule.scheduleTime.day == now.day; + }) + .sortedBy((e) => e.scheduleTime) + .firstOrNull; WeeklySchedulesState copyWith({ WeeklySchedulesStatus Function()? status, diff --git a/lib/presentation/home/components/todays_schedule_tile.dart b/lib/presentation/home/components/todays_schedule_tile.dart index 5c8ccf08..cc14035e 100644 --- a/lib/presentation/home/components/todays_schedule_tile.dart +++ b/lib/presentation/home/components/todays_schedule_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; class TodaysScheduleTile extends StatelessWidget { const TodaysScheduleTile({super.key, this.schedule}); @@ -7,54 +8,51 @@ class TodaysScheduleTile extends StatelessWidget { final ScheduleEntity? schedule; Widget noSchedule(BuildContext context) { final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(11), - ), - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 11.0, vertical: 16.0), - child: Text( - '약속이 없는 날이에요', - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.outlineVariant, - ), - ), + return Text( + '약속이 없는 날이에요', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.outlineVariant, ), ); } Widget scheduleExists(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${schedule!.scheduleTime.hour > 12 ? '오후' : '오전'} ${schedule!.scheduleTime.hour % 12}시 ${schedule!.scheduleTime.minute}분', + style: TextStyle(color: AppColors.white), + ), + Text( + schedule!.scheduleName, + style: TextStyle(color: AppColors.white), + ), + SizedBox(height: 8.0), + // - 시간 - 분 전 + Text( + '${schedule!.scheduleTime.difference(DateTime.now()).inHours}시간 ${schedule!.scheduleTime.difference(DateTime.now()).inMinutes % 60}분 전', + style: TextStyle(color: AppColors.white), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { final theme = Theme.of(context); return Container( decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerLow, + color: schedule == null + ? theme.colorScheme.surfaceContainerLow + : theme.colorScheme.primary, borderRadius: BorderRadius.circular(11), ), width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 11.0, vertical: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - schedule!.scheduleName, - style: theme.textTheme.bodyLarge, - ), - SizedBox(height: 8.0), - Text( - schedule!.place.placeName, - style: theme.textTheme.bodySmall, - ), - ], - ), + child: schedule == null ? noSchedule(context) : scheduleExists(context), ), ); } - - @override - Widget build(BuildContext context) { - return schedule == null ? noSchedule(context) : scheduleExists(context); - } } diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index 1710935d..3e681d50 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; @@ -10,6 +9,7 @@ import 'package:on_time_front/presentation/home/components/todays_schedule_tile. import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/presentation/home/components/week_calendar.dart'; import 'package:on_time_front/presentation/shared/components/arc_indicator.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -18,159 +18,50 @@ class HomeScreen extends StatefulWidget { State createState() => _HomeScreenState(); } -class _HomeScreenState extends State with TickerProviderStateMixin { - late final AnimationController _animationController; - late final Animation _animation; - OverlayEntry? _overlayEntry; - final GlobalKey _overlayKey = GlobalKey(); - - final arrowRightSvg = SvgPicture.asset( - 'assets/arrow_right.svg', - semanticsLabel: 'arrow right', - height: 24, - fit: BoxFit.contain, - ); - +class _HomeScreenState extends State { @override void initState() { - _overlayEntry?.remove(); - _overlayEntry = OverlayEntry( - builder: (context) => todaysScheduleOverlayBuilder(context)); - SchedulerBinding.instance.addPostFrameCallback((_) { - if (_overlayEntry != null) { - Overlay.of(context).insert(_overlayEntry!); - } - }); super.initState(); } - @override - void dispose() { - _overlayEntry?.remove(); - _animationController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final dateOfToday = DateTime( DateTime.now().year, DateTime.now().month, DateTime.now().day, 0, 0, 0); - final theme = Theme.of(context); final double score = context.select((AppBloc bloc) => bloc.state.user.mapOrNull((user) => user.score) ?? -1); - _animationController = AnimationController( - duration: const Duration(seconds: 1), - vsync: this, - )..forward(); - _animation = _animationController.drive( - Tween(begin: 0, end: score / 100), - ); + return BlocProvider( create: (context) => getIt.get() ..add(WeeklySchedulesSubscriptionRequested(date: dateOfToday)), child: Scaffold( appBar: HomeAppBar(), - body: Column( - children: [ - Padding( - key: _overlayKey, - padding: const EdgeInsets.symmetric(horizontal: 32.0), - child: AnimatedBuilder( - animation: _animation, - builder: (context, child) { - return SizedBox( - width: 325, - child: CustomPaint( - painter: ArcIndicator( - strokeWidth: 16, - progress: _animation.value, - ), - child: Center( - child: Padding( - padding: const EdgeInsets.only( - top: 52.0, right: 60.0, left: 60.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('${score.toInt()}점', - style: theme.textTheme.displaySmall), - SizedBox(height: 6.0), - Text( - '성실도 점수 30점 올랐어요!\n약속을 잘 지키고 있네요', - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - SizedBox(height: 6.0), - SizedBox( - height: 270.3, - child: Image.asset('assets/character.png')), - ], - ), - ), - ), - ), - ); - }), - ), - Expanded( - flex: 2, - child: Container( - decoration: BoxDecoration( - color: Color(0xffF3F5FF), - ), - child: Padding( + body: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + _PunctualityIndicator(score: score), + todaysScheduleOverlayBuilder(state), + ], + ), + Expanded( + child: Container( padding: - const EdgeInsets.only(top: 71.0, left: 16.0, right: 16.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('이번 주 약속', style: theme.textTheme.titleSmall), - TextButton( - onPressed: () { - context.go('/calendar'); - }, - child: Row( - children: [ - Text('캘린더 보기', - style: theme.textTheme.bodySmall?.copyWith( - color: - theme.colorScheme.outlineVariant)), - arrowRightSvg, - ], - ), - ), - ], - ), - SizedBox(height: 23.0), - BlocBuilder( - builder: (context, state) { - if (state.schedules.isEmpty) { - if (state.status == WeeklySchedulesStatus.loading) { - return CircularProgressIndicator(); - } else if (state.status != - WeeklySchedulesStatus.success) { - return const SizedBox(); - } - } - - return WeekCalendar( - date: DateTime.now(), - onDateSelected: (date) {}, - highlightedDates: state.dates, - ); - }), - Expanded( - child: SizedBox(), - ), - ], + const EdgeInsets.only(top: 50.0, left: 16.0, right: 16.0), + decoration: BoxDecoration( + color: AppColors.blue[100], + ), + child: _WeeklySchedule( + weeklySchedulesState: state, ), ), ), - ), - ], - ), + ], + ); + }), floatingActionButton: FloatingActionButton( onPressed: () { context.go('/scheduleCreate'); @@ -182,40 +73,257 @@ class _HomeScreenState extends State with TickerProviderStateMixin { ); } - Widget todaysScheduleOverlayBuilder(BuildContext context) { - final keyContext = _overlayKey.currentContext; + Widget todaysScheduleOverlayBuilder(WeeklySchedulesState state) { final theme = Theme.of(context); - if (keyContext != null) { - final RenderBox renderBox = keyContext.findRenderObject() as RenderBox; - final Offset offset = renderBox.localToGlobal(Offset.zero); - return Positioned( - top: offset.dy + renderBox.size.height - 117, - left: 16, - right: 16, - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + + if (state.status == WeeklySchedulesStatus.success) { + return Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + height: 21 + 16, + decoration: BoxDecoration(color: AppColors.blue[100]), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + color: theme.colorScheme.surface, + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '오늘의 약속', + style: theme.textTheme.titleMedium, + ), + SizedBox(height: 21.0), + TodaysScheduleTile( + schedule: state.todaySchedule, + ) + ], + ), + ), ), - color: theme.colorScheme.surface, - elevation: 2, + ), + ], + ); + } else { + return CircularProgressIndicator(); + } + } +} + +class _WeeklySchedule extends StatelessWidget { + const _WeeklySchedule({ + required this.weeklySchedulesState, + }); + + final WeeklySchedulesState weeklySchedulesState; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _WeeklyScheduleHeader(), + SizedBox(height: 23.0), + _WeekCalendar( + weeklySchedulesState: weeklySchedulesState, + ), + Expanded( + child: SizedBox(), + ), + ], + ); + } +} + +class _WeeklyScheduleHeader extends StatelessWidget { + _WeeklyScheduleHeader(); + + final arrowRightSvg = SvgPicture.asset( + 'assets/arrow_right.svg', + semanticsLabel: 'arrow right', + height: 24, + fit: BoxFit.contain, + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('이번 주 약속', style: theme.textTheme.titleSmall), + TextButton( + onPressed: () { + context.go('/calendar'); + }, + child: Row( + children: [ + Text('캘린더 보기', + style: theme.textTheme.bodySmall + ?.copyWith(color: theme.colorScheme.outlineVariant)), + arrowRightSvg, + ], + ), + ), + ], + ); + } +} + +class _WeekCalendar extends StatelessWidget { + const _WeekCalendar({required this.weeklySchedulesState}); + + final WeeklySchedulesState weeklySchedulesState; + + @override + Widget build(BuildContext context) { + if (weeklySchedulesState.schedules.isEmpty) { + if (weeklySchedulesState.status == WeeklySchedulesStatus.loading) { + return CircularProgressIndicator(); + } else if (weeklySchedulesState.status != WeeklySchedulesStatus.success) { + return const SizedBox(); + } + } + + return WeekCalendar( + date: DateTime.now(), + onDateSelected: (date) {}, + highlightedDates: weeklySchedulesState.dates, + ); + } +} + +class _PunctualityIndicator extends StatelessWidget { + const _PunctualityIndicator({ + required this.score, + }); + + final double score; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 40.0), + child: SizedBox( + width: 325, + child: AnimatedArcIndicator( + score: 80, + child: Center( child: Padding( - padding: const EdgeInsets.all(20.0), + padding: + const EdgeInsets.only(top: 52.0, right: 60.0, left: 60.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Text( - '오늘의 약속', - style: theme.textTheme.titleMedium, - ), - SizedBox(height: 21.0), - TodaysScheduleTile( - schedule: null, - ) + _PunctualityScore(score: score), + SizedBox(height: 6.0), + _PunctualityComment( + comment: '성실도 점수 30점 올랐어요!\n약속을 잘 지키고 있네요'), + SizedBox(height: 6.0), + _Character(), ], ), ), - )); - } - return Container(); + ), + ), + ), + ); + } +} + +class AnimatedArcIndicator extends StatefulWidget { + const AnimatedArcIndicator( + {super.key, required this.score, required this.child}); + + final double score; + final Widget child; + + @override + State createState() => _AnimatedArcIndicatorState(); +} + +class _AnimatedArcIndicatorState extends State + with TickerProviderStateMixin { + late final AnimationController _animationController; + late final Animation _animation; + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _animationController = + AnimationController(duration: const Duration(seconds: 1), vsync: this); + _animation = Tween(begin: 0, end: widget.score / 100) + .animate(_animationController); + _animationController.forward(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return CustomPaint( + painter: ArcIndicator( + strokeWidth: 16, + progress: _animation.value, + ), + child: widget.child, + ); + }, + ); + } +} + +class _PunctualityScore extends StatelessWidget { + const _PunctualityScore({ + required this.score, + }); + + final double score; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Text('${score.toInt()}점', style: textTheme.displaySmall); + } +} + +class _Character extends StatelessWidget { + const _Character(); + + @override + Widget build(BuildContext context) { + return SizedBox(height: 270.3, child: Image.asset('assets/character.png')); + } +} + +class _PunctualityComment extends StatelessWidget { + const _PunctualityComment({ + required this.comment, + }); + + final String comment; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Text( + comment, + style: textTheme.bodySmall, + textAlign: TextAlign.center, + ); } } diff --git a/widgetbook/linux/flutter/generated_plugin_registrant.cc b/widgetbook/linux/flutter/generated_plugin_registrant.cc index 72164139..a35cce61 100644 --- a/widgetbook/linux/flutter/generated_plugin_registrant.cc +++ b/widgetbook/linux/flutter/generated_plugin_registrant.cc @@ -6,15 +6,11 @@ #include "generated_plugin_registrant.h" -#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) flutter_js_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterJsPlugin"); - flutter_js_plugin_register_with_registrar(flutter_js_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/widgetbook/linux/flutter/generated_plugins.cmake b/widgetbook/linux/flutter/generated_plugins.cmake index d686577e..2aa89bb0 100644 --- a/widgetbook/linux/flutter/generated_plugins.cmake +++ b/widgetbook/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_js flutter_secure_storage_linux sqlite3_flutter_libs url_launcher_linux diff --git a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift index 3c9b1790..dc3be9f8 100644 --- a/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/widgetbook/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,7 @@ import Foundation import firebase_core import firebase_messaging import flutter_appauth -import flutter_js +import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth import google_sign_in_ios @@ -22,7 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterAppauthPlugin.register(with: registry.registrar(forPlugin: "FlutterAppauthPlugin")) - FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) diff --git a/widgetbook/pubspec.lock b/widgetbook/pubspec.lock index 452a5dab..f2535845 100644 --- a/widgetbook/pubspec.lock +++ b/widgetbook/pubspec.lock @@ -216,6 +216,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" device_frame: dependency: transitive description: @@ -389,14 +397,6 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.0" - flutter_js: - dependency: transitive - description: - name: flutter_js - sha256: "6b777cd4e468546f046a2f114d078a4596143269f6fa6bad5c29611d5b896369" - url: "https://pub.dev" - source: hosted - version: "0.8.2" flutter_lints: dependency: "direct dev" description: @@ -405,6 +405,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_local_notifications: + dependency: transitive + description: + name: flutter_local_notifications + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + url: "https://pub.dev" + source: hosted + version: "18.0.1" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + url: "https://pub.dev" + source: hosted + version: "8.0.0" flutter_riverpod: dependency: transitive description: @@ -1179,14 +1203,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" - source: hosted - version: "0.3.1" table_calendar: dependency: transitive description: @@ -1211,6 +1227,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + timezone: + dependency: transitive + description: + name: timezone + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + url: "https://pub.dev" + source: hosted + version: "0.10.0" timing: dependency: transitive description: diff --git a/widgetbook/windows/flutter/generated_plugin_registrant.cc b/widgetbook/windows/flutter/generated_plugin_registrant.cc index d5e562d7..aa588b48 100644 --- a/widgetbook/windows/flutter/generated_plugin_registrant.cc +++ b/widgetbook/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -15,8 +14,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); - FlutterJsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterJsPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( diff --git a/widgetbook/windows/flutter/generated_plugins.cmake b/widgetbook/windows/flutter/generated_plugins.cmake index 4e45dad5..6967ac89 100644 --- a/widgetbook/windows/flutter/generated_plugins.cmake +++ b/widgetbook/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core - flutter_js flutter_secure_storage_windows sqlite3_flutter_libs url_launcher_windows