diff --git a/android/app/build.gradle b/android/app/build.gradle index 3d41f167..15b6b3e4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -52,7 +52,7 @@ android { defaultConfig { applicationId "com.w8385.my_solved" - minSdkVersion 34 + minSdkVersion flutter.minSdkVersion targetSdkVersion 34 multiDexEnabled true versionCode flutterVersionCode.toInteger() diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d330d2b7..f275ee81 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + + + + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5a7e87d6..b630814b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -378,8 +378,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3PFDRPLY6N; + CURRENT_PROJECT_VERSION = 74; + DEVELOPMENT_TEAM = 2U3M8CAZBZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MY.SOLVED; @@ -387,7 +387,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = com.jeehoukjung.mySolved; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -514,8 +514,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3PFDRPLY6N; + CURRENT_PROJECT_VERSION = 74; + DEVELOPMENT_TEAM = 2U3M8CAZBZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MY.SOLVED; @@ -523,7 +523,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = com.jeehoukjung.mySolved; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -544,8 +544,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 3PFDRPLY6N; + CURRENT_PROJECT_VERSION = 74; + DEVELOPMENT_TEAM = 2U3M8CAZBZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MY.SOLVED; @@ -553,7 +553,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.0.5; PRODUCT_BUNDLE_IDENTIFIER = com.jeehoukjung.mySolved; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 351c9ff4..882a0c13 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -110,5 +110,8 @@ UIApplicationSupportsIndirectInputEvents + + NSCalendarsUsageDescription + adds contest to calendar diff --git a/lib/features/contest/bloc/contest_bloc.dart b/lib/features/contest/bloc/contest_bloc.dart index 0f85eae9..5e581c14 100644 --- a/lib/features/contest/bloc/contest_bloc.dart +++ b/lib/features/contest/bloc/contest_bloc.dart @@ -1,13 +1,17 @@ +import 'package:add_2_calendar/add_2_calendar.dart'; import 'package:boj_api/boj_api.dart'; import 'package:contest_notification_repository/contest_notification_repository.dart'; import 'package:contest_repository/contest_repository.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:meta/meta.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:my_solved/features/contest_filter/bloc/contest_filter_bloc.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences_repository/shared_preferences_repository.dart'; +import '../../../components/styles/color.dart'; + part 'contest_event.dart'; part 'contest_state.dart'; @@ -31,6 +35,7 @@ class ContestBloc extends Bloc { on(_onInit); on(_onSegmentedControlPressed); on(_onNotificationPressed); + on(_onCalendarPressed); on(_onFilterPressed); } @@ -48,9 +53,12 @@ class ContestBloc extends Bloc { final upcomingContests = result[ContestType.upcoming]; List isOnContestNotifications = []; + List isOnContestCalendars = []; for (Contest contest in upcomingContests ?? []) { isOnContestNotifications.add(await _sharedPreferencesRepository .getIsOnContestNotification(title: contest.name)); + isOnContestCalendars.add(await _sharedPreferencesRepository + .getIsOnContestCalendar(title: contest.name)); } emit(state.copyWith( @@ -59,6 +67,7 @@ class ContestBloc extends Bloc { ongoingContests: ongoingContests, upcomingContests: upcomingContests, isOnNotificationUpcomingContests: isOnContestNotifications, + isOnCalendarUpcomingContests: isOnContestCalendars, )); } catch (e) { emit(state.copyWith(status: ContestStatus.failure)); @@ -90,6 +99,15 @@ class ContestBloc extends Bloc { final minute = await _sharedPreferencesRepository.getContestNotificationMinute(); + Fluttertoast.showToast( + msg: isOn ? "알람이 취소되었습니다." : "알람이 설정되었습니다.", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: MySolvedColor.main.withOpacity(0.8), + textColor: Colors.white, + fontSize: 16.0); + if (isOn) { await _contestNotificationRepository.cancelContestNotification( title: contest.name, @@ -121,6 +139,63 @@ class ContestBloc extends Bloc { } } + void _onCalendarPressed( + ContestCalendarButtonPressed event, + Emitter emit, + ) async { + emit(state.copyWith(status: ContestStatus.loading)); + + final contest = state.filteredUpcomingContests[event.index]; + List isOnCalendar = state.isOnCalendarUpcomingContests; + final isOn = await _sharedPreferencesRepository.getIsOnContestCalendar( + title: contest.name, + ); + + if (isOn) { + await _sharedPreferencesRepository.setIsOnContestCalendar( + title: contest.name, + isOn: false, + ); + isOnCalendar[event.index] = false; + emit(state.copyWith( + status: ContestStatus.success, + isOnCalendarUpcomingContests: isOnCalendar, + )); + } else { + Fluttertoast.showToast( + msg: "일정을 등록합니다.", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: MySolvedColor.main.withOpacity(0.8), + textColor: Colors.white, + fontSize: 16.0); + + final Event calendarEvent = Event( + title: contest.name, + description: contest.url, + startDate: contest.startTime, + endDate: contest.endTime, + iosParams: IOSParams( + url: contest.url, + ), + ); + Add2Calendar.addEvent2Cal(calendarEvent); + + await _sharedPreferencesRepository.setIsOnContestCalendar( + title: contest.name, + isOn: true, + ); + isOnCalendar[event.index] = true; + emit(state.copyWith( + status: ContestStatus.success, + isOnCalendarUpcomingContests: isOnCalendar, + )); + } + + emit(state.copyWith(status: ContestStatus.success)); + } + void _onSegmentedControlPressed( ContestSegmentedControlPressed event, Emitter emit, diff --git a/lib/features/contest/bloc/contest_event.dart b/lib/features/contest/bloc/contest_event.dart index c78cb89b..3e1f3d34 100644 --- a/lib/features/contest/bloc/contest_event.dart +++ b/lib/features/contest/bloc/contest_event.dart @@ -17,6 +17,12 @@ class ContestNotificationButtonPressed extends ContestEvent { ContestNotificationButtonPressed({required this.index}); } +class ContestCalendarButtonPressed extends ContestEvent { + final int index; + + ContestCalendarButtonPressed({required this.index}); +} + class ContestFilterTogglePressed extends ContestEvent { final ContestVenue venue; diff --git a/lib/features/contest/bloc/contest_state.dart b/lib/features/contest/bloc/contest_state.dart index 7f16b454..4eaa1281 100644 --- a/lib/features/contest/bloc/contest_state.dart +++ b/lib/features/contest/bloc/contest_state.dart @@ -4,8 +4,11 @@ enum ContestStatus { initial, loading, success, failure } extension ContestStatusX on ContestStatus { bool get isInitial => this == ContestStatus.initial; + bool get isLoading => this == ContestStatus.loading; + bool get isSuccess => this == ContestStatus.success; + bool get isFailure => this == ContestStatus.failure; } @@ -16,18 +19,22 @@ class ContestState extends Equatable { final List ongoingContests; final List upcomingContests; final List isOnNotificationUpcomingContests; + final List isOnCalendarUpcomingContests; final Map filters; List get selectedVenues => ContestVenue.allCases .where((venue) => filters[venue] ?? false) .map((venue) => venue.value) .toList(); + List get filteredEndedContests => endedContests .where((contest) => selectedVenues.contains(contest.venue)) .toList(); + List get filteredOngoingContests => ongoingContests .where((contest) => selectedVenues.contains(contest.venue)) .toList(); + List get filteredUpcomingContests => upcomingContests .where((contest) => selectedVenues.contains(contest.venue)) .toList(); @@ -39,6 +46,7 @@ class ContestState extends Equatable { this.ongoingContests = const [], this.upcomingContests = const [], this.isOnNotificationUpcomingContests = const [], + this.isOnCalendarUpcomingContests = const [], required this.filters, }); @@ -49,6 +57,7 @@ class ContestState extends Equatable { List? ongoingContests, List? upcomingContests, List? isOnNotificationUpcomingContests, + List? isOnCalendarUpcomingContests, Map? filters, }) { return ContestState( @@ -59,6 +68,8 @@ class ContestState extends Equatable { upcomingContests: upcomingContests ?? this.upcomingContests, isOnNotificationUpcomingContests: isOnNotificationUpcomingContests ?? this.isOnNotificationUpcomingContests, + isOnCalendarUpcomingContests: + isOnCalendarUpcomingContests ?? this.isOnCalendarUpcomingContests, filters: filters ?? this.filters, ); } @@ -71,10 +82,11 @@ class ContestState extends Equatable { ongoingContests, upcomingContests, isOnNotificationUpcomingContests, + isOnCalendarUpcomingContests, filters, selectedVenues, filteredEndedContests, filteredOngoingContests, filteredUpcomingContests, ]; -} \ No newline at end of file +} diff --git a/lib/features/contest/screen/contest_screen.dart b/lib/features/contest/screen/contest_screen.dart index 6ca1024b..908b5acf 100644 --- a/lib/features/contest/screen/contest_screen.dart +++ b/lib/features/contest/screen/contest_screen.dart @@ -2,7 +2,7 @@ import 'package:boj_api/boj_api.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:my_solved/components/atoms/button/button.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:my_solved/components/molecules/segmented_control/segmented_control.dart'; import 'package:my_solved/components/styles/color.dart'; import 'package:my_solved/components/styles/font.dart'; @@ -124,12 +124,7 @@ class _ContestViewState extends State { (index) => Column( children: [ ElevatedButton( - onPressed: () async { - final urlString = contests[index].url; - if (urlString != null) { - launchUrlString(urlString); - } - }, + onPressed: () {}, style: ElevatedButton.styleFrom( minimumSize: Size.fromHeight(128), padding: EdgeInsets.all(16), @@ -157,78 +152,129 @@ class _ContestViewState extends State { color: MySolvedColor.secondaryFont, ), ), - SizedBox(height: 8), - Row( - children: [ - if (state.currentIndex == 1) - MySolvedFitButton( - onPressed: () => context - .read() - .add(ContestNotificationButtonPressed( - index: index, + Container( + margin: EdgeInsets.only(top: 8), + height: 50, + child: Row( + children: [ + if (state.currentIndex == 1) + IconButton( + onPressed: () => context + .read() + .add( + ContestNotificationButtonPressed( + index: index, + )), + style: ButtonStyle( + backgroundColor: state + .isOnNotificationUpcomingContests[ + index] + ? MaterialStateProperty.all( + MySolvedColor + .secondaryButtonBackground) + : MaterialStateProperty.all( + MySolvedColor.main), + ), + icon: Icon( + state.isOnNotificationUpcomingContests[ + index] + ? Icons.alarm_off + : Icons.alarm, + color: + state.isOnNotificationUpcomingContests[ + index] + ? MySolvedColor.secondaryFont + : MySolvedColor.background, + )), + if (state.currentIndex == 1) + IconButton( + onPressed: () { + context.read().add( + ContestCalendarButtonPressed( + index: index)); + }, + style: ButtonStyle( + backgroundColor: state + .isOnCalendarUpcomingContests[ + index] + ? MaterialStateProperty.all( + MySolvedColor + .secondaryButtonBackground) + : MaterialStateProperty.all( + MySolvedColor.main), + ), + icon: Icon( + Icons.calendar_month, + color: + state.isOnCalendarUpcomingContests[ + index] + ? MySolvedColor.secondaryFont + : MySolvedColor.background, + size: 20, )), - buttonStyle: - state.isOnNotificationUpcomingContests[ - index] - ? MySolvedButtonStyle.secondary - : MySolvedButtonStyle.primary, - text: - state.isOnNotificationUpcomingContests[ - index] - ? "알림 취소하기" - : "알림 설정하기", - ), - Spacer(), - if (contests[index].badge != null) - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - elevation: 0, - minimumSize: Size.zero, - padding: EdgeInsets.symmetric( - vertical: 8, horizontal: 12), - foregroundColor: Color(0xFFfab005), + Spacer(), + if (contests[index].badge != null) + IconButton( + onPressed: () => Fluttertoast.showToast( + msg: + "${contests[index].badge!}시 뱃지 획득", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: MySolvedColor.main + .withOpacity(0.8), + textColor: Colors.white, + fontSize: 16.0), + style: IconButton.styleFrom( + foregroundColor: Color(0xFFfab005), + backgroundColor: MySolvedColor + .secondaryBackground), + icon: Icon(Icons.badge), ), - child: Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: contests[index].badge, - child: Text( - '🏅', - style: MySolvedTextStyle.caption1, + if (contests[index].background != null) + IconButton( + onPressed: () => Fluttertoast.showToast( + msg: + "${contests[index].background!}시 배경 획득", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: MySolvedColor.main + .withOpacity(0.8), + textColor: Colors.white, + fontSize: 16.0), + style: IconButton.styleFrom( + foregroundColor: Color(0xFFb197fc), + backgroundColor: + MySolvedColor.secondaryBackground, ), + icon: Icon(Icons.image), ), - ), - if (contests[index].background != null) - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - elevation: 0, - minimumSize: Size.zero, - padding: EdgeInsets.symmetric( - vertical: 8, horizontal: 12), - foregroundColor: Color(0xFFb197fc), + IconButton( + onPressed: () async { + final urlString = contests[index].url; + if (urlString != null) { + launchUrlString(urlString); + } + }, + style: IconButton.styleFrom( + backgroundColor: + MySolvedColor.secondaryBackground, ), - child: Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: contests[index].background, - child: Text( - '🖼️', - style: MySolvedTextStyle.caption1, - ), + icon: ExtendedImage.asset( + "assets/images/venues/${contests[index].venue?.toLowerCase() ?? "etc"}.png", + width: 24, ), - ), - ExtendedImage.asset( - "assets/images/venues/${contests[index].venue?.toLowerCase() ?? "etc"}.png", - height: 30, - width: 30, - ), - ], + ) + ], + ), ), - if (state.currentIndex == 0) SizedBox(height: 8), if (state.currentIndex == 0) - ProgressIndicator( - contests[index].startTime.toLocal(), - contests[index].endTime.toLocal()), + Container( + margin: EdgeInsets.only(top: 8), + child: ProgressIndicator( + contests[index].startTime.toLocal(), + contests[index].endTime.toLocal())), ], ), ), diff --git a/packages/repositories/shared_preferences_repository/lib/src/shared_preferences_repository.dart b/packages/repositories/shared_preferences_repository/lib/src/shared_preferences_repository.dart index acaeab9a..d95e0d39 100644 --- a/packages/repositories/shared_preferences_repository/lib/src/shared_preferences_repository.dart +++ b/packages/repositories/shared_preferences_repository/lib/src/shared_preferences_repository.dart @@ -107,4 +107,15 @@ class SharedPreferencesRepository { }) async { await _sharedPreferencesApiClient.setBool(key: title, value: isOn); } -} \ No newline at end of file + + Future getIsOnContestCalendar({required String title}) async { + return await _sharedPreferencesApiClient.getBool(key: title) ?? false; + } + + Future setIsOnContestCalendar({ + required String title, + required bool isOn, + }) async { + await _sharedPreferencesApiClient.setBool(key: title, value: isOn); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index c98f6582..430c8915 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: For solving problems in the world of programming; base on solved.ac publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 2.0.5+74 +version: 2.1.0+78 environment: sdk: ">=2.18.2 <3.0.0" @@ -16,7 +16,6 @@ dependencies: equatable: ^2.0.5 extended_image: ^8.1.0 fluttertoast: ^8.2.8 - flutter_app_badger: ^1.5.0 flutter_bloc: ^8.1.3 flutter_local_notifications: ^13.0.0 flutter_native_timezone: ^2.0.0 @@ -34,6 +33,7 @@ dependencies: timezone: ^0.9.1 url_launcher: ^6.1.10 permission_handler: ^11.3.1 + add_2_calendar: ^3.0.1 boj_api: path: packages/apis/boj_api @@ -61,6 +61,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.1 + build_web_compilers: ^4.0.4 flutter: uses-material-design: true diff --git a/web/favicon.png b/web/favicon.png index 8aaa46ac..8796d2b1 100644 Binary files a/web/favicon.png and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfef..8ddc28c4 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png old mode 100644 new mode 100755 index 88cfd48d..9ffba617 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d76..8ddc28c4 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png old mode 100644 new mode 100755 index d69c5669..9ffba617 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html index 4582a5a3..4aece673 100644 --- a/web/index.html +++ b/web/index.html @@ -1,46 +1,62 @@ - - - - - - - - - - - - - - - - - my_solved - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + my_solved + + + + + - + diff --git a/web/manifest.json b/web/manifest.json index 8187dff0..9bdbc720 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,35 +1,35 @@ { - "name": "my_solved", - "short_name": "my_solved", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] + "name": "my_solved", + "short_name": "my_solved", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "보다 편한 PS/CP를 위한 애플리케이션", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] }